diff --git a/config/nacos/cn/finance-service.yml b/config/nacos/cn/finance-service.yml index 75979d525..aa39d45b0 100644 --- a/config/nacos/cn/finance-service.yml +++ b/config/nacos/cn/finance-service.yml @@ -5,6 +5,5 @@ spring: password: ${FINANCE_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:} -baiyue.finance.recharge.wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:} -baiyue.finance.recharge.alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:} +baiyue.finance.recharge.qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:} baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00} diff --git a/config/nacos/local/finance-service.yml b/config/nacos/local/finance-service.yml index 28e288d48..6269c2f7a 100644 --- a/config/nacos/local/finance-service.yml +++ b/config/nacos/local/finance-service.yml @@ -5,6 +5,5 @@ spring: username: ${FINANCE_DB_USERNAME:root} password: ${FINANCE_DB_PASSWORD:123456} baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:} -baiyue.finance.recharge.wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:/customer-recharge-qr.svg} -baiyue.finance.recharge.alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:/customer-recharge-qr.svg} +baiyue.finance.recharge.qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:/customer-recharge-qr.svg} baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00} diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 61903f0a2..9bc6efa61 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -4908,7 +4908,6 @@ CREATE TABLE fin_recharge_request ( customer_id bigint unsigned NOT NULL COMMENT '客户ID和隔离键', request_no varchar(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####', amount decimal(20,2) NOT NULL COMMENT '申请充值金额', - payment_method varchar(16) NOT NULL COMMENT 'WECHAT或ALIPAY', voucher_file_id bigint unsigned NOT NULL COMMENT '付款截图File ID', voucher_file_name varchar(255) NOT NULL COMMENT '付款截图文件名快照', status varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', @@ -4926,7 +4925,6 @@ CREATE TABLE fin_recharge_request ( KEY idx_fin_recharge_customer_status (customer_id,status,created_at,id), CONSTRAINT chk_fin_recharge_amount CHECK ((amount > 0)), CONSTRAINT chk_fin_recharge_customer CHECK ((customer_id > 0)), - CONSTRAINT chk_fin_recharge_method CHECK ((payment_method in (_utf8mb4'WECHAT',_utf8mb4'ALIPAY'))), CONSTRAINT chk_fin_recharge_status CHECK ((status in (_utf8mb4'PENDING',_utf8mb4'APPROVED',_utf8mb4'REJECTED'))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户付款截图充值申请;平台审核通过后入账'; diff --git a/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceWalletController.java b/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceWalletController.java index 2f6d44b2e..6d4c29f44 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceWalletController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceWalletController.java @@ -40,19 +40,18 @@ public class ClientFinanceWalletController { return FinanceResponses.success(service.rechargeRequests(user.tenantId(), page, size)); } - @GetMapping("/recharge-channels") + @GetMapping("/recharge-channel") @PreAuthorize("hasAuthority('merchant:finance:recharge:create')") - public ApiResponse channels() { return FinanceResponses.success(service.channels()); } + public ApiResponse channel() { return FinanceResponses.success(service.channel()); } @PostMapping(value = "/recharge-requests", consumes = "multipart/form-data") @PreAuthorize("hasAuthority('merchant:finance:recharge:create')") public ApiResponse submit(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String idempotencyKey, @RequestParam BigDecimal amount, - @RequestParam String paymentMethod, @RequestPart("file") MultipartFile file) { return FinanceResponses.success(service.submitRecharge(user.tenantId(), user.userId(), user.displayName(), - idempotencyKey, amount, paymentMethod, file)); + idempotencyKey, amount, file)); } @GetMapping("/recharge-requests/{requestId}/voucher") diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java index 9f48b1313..db7893993 100644 --- a/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java +++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java @@ -33,13 +33,12 @@ public final class FinanceWalletDtos { String remark, LocalDateTime createdAt) { } public record RechargeRequest(long requestId, String requestNo, BigDecimal amount, - String paymentMethod, long voucherFileId, String voucherFileName, + long voucherFileId, String voucherFileName, String status, int version, String submittedByName, String reviewedByName, String reviewRemark, LocalDateTime createdAt, LocalDateTime reviewedAt) { } - public record RechargeChannel(String paymentMethod, String displayName, - boolean enabled, String qrCodeUrl) { } + public record RechargeChannel(boolean enabled, String qrCodeUrl) { } public record RechargeReviewRequest(String decision, String remark, int expectedVersion) { } diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceWalletService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceWalletService.java index 0d1f81a09..6a1d3475e 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceWalletService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceWalletService.java @@ -42,7 +42,6 @@ import java.util.Set; @Service public class FinanceWalletService { - private static final Set PAYMENT_METHODS = Set.of("WECHAT", "ALIPAY"); private static final Set ADJUSTMENT_PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER"); private static final Set IMAGE_TYPES = Set.of("image/jpeg", "image/png"); @@ -51,15 +50,13 @@ public class FinanceWalletService { private final FinanceFileClient fileClient; private final FinanceDocumentNumberGenerator numberGenerator; private final String internalToken; - private final String wechatQrCodeUrl; - private final String alipayQrCodeUrl; + private final String rechargeQrCodeUrl; public FinanceWalletService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceFileClient fileClient, FinanceDocumentNumberGenerator numberGenerator, @Value("${baiyue.security.internal-service-token:}") String internalToken, - @Value("${baiyue.finance.recharge.wechat-qr-code-url:}") String wechatQrCodeUrl, - @Value("${baiyue.finance.recharge.alipay-qr-code-url:}") String alipayQrCodeUrl, + @Value("${baiyue.finance.recharge.qr-code-url:}") String rechargeQrCodeUrl, @Value("${spring.profiles.active:}") String activeProfiles, @Value("${DEPLOY_ENV:}") String deployEnvironment) { this.jdbc = jdbc; @@ -69,8 +66,7 @@ public class FinanceWalletService { this.internalToken = internalToken; boolean development = "development".equalsIgnoreCase(deployEnvironment.trim()) || List.of(activeProfiles.split(",")).stream().map(String::trim).anyMatch("dev"::equalsIgnoreCase); - this.wechatQrCodeUrl = rechargeQrCode(wechatQrCodeUrl, development); - this.alipayQrCodeUrl = rechargeQrCode(alipayQrCodeUrl, development); + this.rechargeQrCodeUrl = rechargeQrCode(rechargeQrCodeUrl, development); } private static String rechargeQrCode(String configured, boolean development) { @@ -104,22 +100,20 @@ public class FinanceWalletService { return WalletPage.of(rows, total == null ? 0 : total, page, size); } - public List channels() { - return List.of(new RechargeChannel("WECHAT", "微信支付", wechatQrCodeUrl != null, wechatQrCodeUrl), - new RechargeChannel("ALIPAY", "支付宝", alipayQrCodeUrl != null, alipayQrCodeUrl)); + public RechargeChannel channel() { + return new RechargeChannel(rechargeQrCodeUrl != null, rechargeQrCodeUrl); } @Transactional public RechargeRequest submitRecharge(long customerId, long operatorId, String operatorName, String idempotencyKey, BigDecimal amount, - String paymentMethod, MultipartFile file) { + MultipartFile file) { requirePositive(customerId, "客户"); requirePositive(operatorId, "操作人"); BigDecimal normalizedAmount = money(amount); - String method = paymentMethod(paymentMethod); - RechargeChannel channel = channels().stream().filter(value -> value.paymentMethod().equals(method)).findFirst().orElseThrow(); - if (!channel.enabled()) throw error("FIN-WALLET-CHANNEL-DISABLED", channel.displayName() + "收款二维码未配置"); + RechargeChannel channel = channel(); + if (!channel.enabled()) throw error("FIN-WALLET-CHANNEL-DISABLED", "收款二维码未配置"); validateImage(file); - String requestHash = hash(Map.of("amount", normalizedAmount.toPlainString(), "paymentMethod", method, + String requestHash = hash(Map.of("amount", normalizedAmount.toPlainString(), "fileName", String.valueOf(file.getOriginalFilename()), "contentType", String.valueOf(file.getContentType()), "size", file.getSize(), "fileHash", fileHash(file))); Long replay = reserve(customerId, operatorId, "WALLET_RECHARGE_SUBMIT", idempotencyKey, requestHash); @@ -138,12 +132,12 @@ public class FinanceWalletService { KeyHolder holder = new GeneratedKeyHolder(); jdbc.update(connection -> { PreparedStatement statement = connection.prepareStatement( - "INSERT INTO fin_recharge_request(customer_id,request_no,amount,payment_method,voucher_file_id,voucher_file_name,status,version,submitted_by,submitted_by_name) VALUES(?,?,?,?,?,?,'PENDING',0,?,?)", + "INSERT INTO fin_recharge_request(customer_id,request_no,amount,voucher_file_id,voucher_file_name,status,version,submitted_by,submitted_by_name) VALUES(?,?,?,?,?,'PENDING',0,?,?)", Statement.RETURN_GENERATED_KEYS); statement.setLong(1, customerId); statement.setString(2, requestNo); - statement.setBigDecimal(3, normalizedAmount); statement.setString(4, method); - statement.setLong(5, upload.data().fileId()); statement.setString(6, upload.data().fileName()); - statement.setLong(7, operatorId); statement.setString(8, clean(operatorName, 128)); + statement.setBigDecimal(3, normalizedAmount); + statement.setLong(4, upload.data().fileId()); statement.setString(5, upload.data().fileName()); + statement.setLong(6, operatorId); statement.setString(7, clean(operatorName, 128)); return statement; }, holder); long requestId = generatedId(holder); @@ -173,7 +167,7 @@ public class FinanceWalletService { WalletAccount afterAccount = rechargeAccount(account, before.amount()); updateAccount(beforeCustomerId(requestId), afterAccount, account.version(), operatorId); insertLedger(beforeCustomerId(requestId), "RECHARGE", before.amount(), afterAccount, - "CLIENT_RECHARGE", before.requestNo(), LocalDate.now(), before.paymentMethod(), null, + "CLIENT_RECHARGE", before.requestNo(), LocalDate.now(), null, null, operatorId, operatorName, request.remark()); } RechargeRequest after = requiredRecharge(requestId, null); @@ -587,7 +581,7 @@ public class FinanceWalletService { private RechargeRequest rechargeRow(java.sql.ResultSet rs) throws java.sql.SQLException { return new RechargeRequest(rs.getLong("id"), rs.getString("request_no"), rs.getBigDecimal("amount"), - rs.getString("payment_method"), rs.getLong("voucher_file_id"), rs.getString("voucher_file_name"), + rs.getLong("voucher_file_id"), rs.getString("voucher_file_name"), rs.getString("status"), rs.getInt("version"), rs.getString("submitted_by_name"), rs.getString("reviewed_by_name"), rs.getString("review_remark"), timestamp(rs.getTimestamp("created_at")), timestamp(rs.getTimestamp("reviewed_at"))); @@ -633,11 +627,6 @@ public class FinanceWalletService { if (normalized == null) throw bad("业务单号不能为空"); return normalized; } - private static String paymentMethod(String value) { - String normalized = upper(value); - if (!PAYMENT_METHODS.contains(normalized)) throw bad("付款方式无效"); - return normalized; - } private static void validateImage(MultipartFile file) { if (file == null || file.isEmpty()) throw bad("请上传付款截图"); if (!IMAGE_TYPES.contains(String.valueOf(file.getContentType()).toLowerCase(Locale.ROOT))) throw bad("付款截图仅支持 JPG 或 PNG"); diff --git a/finance/src/main/resources/application-dev.yml b/finance/src/main/resources/application-dev.yml index d834f00bb..776bc5372 100644 --- a/finance/src/main/resources/application-dev.yml +++ b/finance/src/main/resources/application-dev.yml @@ -8,5 +8,4 @@ spring: baiyue: finance: recharge: - wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:/customer-recharge-qr.svg} - alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:/customer-recharge-qr.svg} + qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:/customer-recharge-qr.svg} diff --git a/finance/src/main/resources/db/finance.sql b/finance/src/main/resources/db/finance.sql index d704277da..d82343879 100644 --- a/finance/src/main/resources/db/finance.sql +++ b/finance/src/main/resources/db/finance.sql @@ -467,7 +467,6 @@ CREATE TABLE IF NOT EXISTS fin_recharge_request ( customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键', request_no VARCHAR(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####', amount DECIMAL(20,2) NOT NULL COMMENT '申请充值金额', - payment_method VARCHAR(16) NOT NULL COMMENT 'WECHAT或ALIPAY', voucher_file_id BIGINT UNSIGNED NOT NULL COMMENT '付款截图File ID', voucher_file_name VARCHAR(255) NOT NULL COMMENT '付款截图文件名快照', status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', @@ -485,7 +484,6 @@ CREATE TABLE IF NOT EXISTS fin_recharge_request ( KEY idx_fin_recharge_customer_status (customer_id, status, created_at, id), CONSTRAINT chk_fin_recharge_customer CHECK (customer_id > 0), CONSTRAINT chk_fin_recharge_amount CHECK (amount > 0), - CONSTRAINT chk_fin_recharge_method CHECK (payment_method IN ('WECHAT','ALIPAY')), CONSTRAINT chk_fin_recharge_status CHECK (status IN ('PENDING','APPROVED','REJECTED')) ) ENGINE=InnoDB COMMENT='客户付款截图充值申请;平台审核通过后入账'; diff --git a/finance/src/test/java/com/baiyue/finance/service/FinanceWalletBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/service/FinanceWalletBehaviorTest.java index 4a37ce725..ed8338db6 100644 --- a/finance/src/test/java/com/baiyue/finance/service/FinanceWalletBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/service/FinanceWalletBehaviorTest.java @@ -22,7 +22,7 @@ class FinanceWalletBehaviorTest { JdbcTemplate jdbc = mock(JdbcTemplate.class); when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of()); - var account = service(jdbc, "https://pay.example/wechat.png", "").account(9L); + var account = service(jdbc, "https://pay.example/qr.png").account(9L); assertThat(account.actualBalance()).isEqualByComparingTo("0.00"); assertThat(account.frozenBalance()).isEqualByComparingTo("0.00"); @@ -34,28 +34,25 @@ class FinanceWalletBehaviorTest { @Test void rechargeChannelsAreEnabledOnlyWhenQrCodeIsConfigured() { - var channels = service(mock(JdbcTemplate.class), "https://pay.example/wechat.png", "").channels(); + var channel = service(mock(JdbcTemplate.class), "https://pay.example/qr.png").channel(); - assertThat(channels).satisfiesExactly( - wechat -> { assertThat(wechat.paymentMethod()).isEqualTo("WECHAT"); assertThat(wechat.enabled()).isTrue(); }, - alipay -> { assertThat(alipay.paymentMethod()).isEqualTo("ALIPAY"); assertThat(alipay.enabled()).isFalse(); }); + assertThat(channel.enabled()).isTrue(); + assertThat(channel.qrCodeUrl()).isEqualTo("https://pay.example/qr.png"); } @Test void developmentProfileUsesTheNonPayableSampleQrCode() { var channels = new FinanceWalletService(mock(JdbcTemplate.class), new ObjectMapper().findAndRegisterModules(), - mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", "", "", - "cn", "development").channels(); + mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", "", + "cn", "development").channel(); - assertThat(channels).allSatisfy(channel -> { - assertThat(channel.enabled()).isTrue(); - assertThat(channel.qrCodeUrl()).isEqualTo("/customer-recharge-qr.svg"); - }); + assertThat(channels.enabled()).isTrue(); + assertThat(channels.qrCodeUrl()).isEqualTo("/customer-recharge-qr.svg"); } @Test void deductionMayMakeActualBalanceNegative() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount initial = service.accountState(new BigDecimal("10.00"), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, null, 2); @@ -71,7 +68,7 @@ class FinanceWalletBehaviorTest { JdbcTemplate jdbc = mock(JdbcTemplate.class); when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of()); - var account = service(jdbc, "x", "y").account(9L); + var account = service(jdbc, "x").account(9L); assertThat(account.creditLimit()).isZero(); assertThat(account.availableCredit()).isZero(); @@ -82,7 +79,7 @@ class FinanceWalletBehaviorTest { @Test void deductionUsesCashBeforeCreditAndRechargeRepaysCreditFirst() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount initial = service.accountState(new BigDecimal("100.00"), BigDecimal.ZERO, new BigDecimal("50.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 3); @@ -100,7 +97,7 @@ class FinanceWalletBehaviorTest { @Test void deductionNeverConsumesCashReservedByAnActiveHold() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount initial = service.accountState(new BigDecimal("100.00"), new BigDecimal("50.00"), new BigDecimal("80.00"), new BigDecimal("20.00"), LocalDate.now().plusDays(7), 4); @@ -116,7 +113,7 @@ class FinanceWalletBehaviorTest { @Test void rechargeRepaysUsedCreditWithoutReleasingFrozenCredit() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount initial = service.accountState(new BigDecimal("5.00"), new BigDecimal("25.00"), new BigDecimal("100.00"), new BigDecimal("40.00"), new BigDecimal("20.00"), LocalDate.now().plusDays(7), 5); @@ -133,7 +130,7 @@ class FinanceWalletBehaviorTest { @Test void accountStateRejectsCreditUsedAndFrozenAboveTheLimit() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); assertThatThrownBy(() -> service.accountState(BigDecimal.ZERO, new BigDecimal("20.00"), new BigDecimal("50.00"), new BigDecimal("31.00"), new BigDecimal("20.00"), @@ -144,7 +141,7 @@ class FinanceWalletBehaviorTest { @Test void accountStateAllowsBalanceToBecomeNegativeWhileFundsRemainFrozen() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount account = service.accountState(new BigDecimal("9.99"), new BigDecimal("10.00"), new BigDecimal("20.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 1); @@ -155,7 +152,7 @@ class FinanceWalletBehaviorTest { @Test void accountStateRejectsCreditFrozenAboveTotalFrozenAmount() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); assertThatThrownBy(() -> service.accountState(new BigDecimal("100.00"), new BigDecimal("10.00"), new BigDecimal("50.00"), new BigDecimal("10.01"), LocalDate.now().plusDays(7), 1)) @@ -165,7 +162,7 @@ class FinanceWalletBehaviorTest { @Test void expiredCreditNoLongerContributesToAvailableBalance() { - FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); + FinanceWalletService service = service(mock(JdbcTemplate.class), "x"); WalletAccount account = service.accountState(BigDecimal.ZERO, BigDecimal.ZERO, new BigDecimal("50.00"), new BigDecimal("20.00"), BigDecimal.ZERO, @@ -176,9 +173,9 @@ class FinanceWalletBehaviorTest { assertThat(account.availableBalance()).isEqualByComparingTo("-20.00"); } - private static FinanceWalletService service(JdbcTemplate jdbc, String wechat, String alipay) { + private static FinanceWalletService service(JdbcTemplate jdbc, String qrCodeUrl) { return new FinanceWalletService(jdbc, new ObjectMapper().findAndRegisterModules(), - mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", wechat, alipay, + mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", qrCodeUrl, "test", "test"); } }