From a7c8a8f6055e1a03414f2227d12ff2e87ec52d41 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期五, 21 八月 2026 10:38:23 +0800
Subject: [PATCH] update
---
backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalSummaryResponse.java | 13
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxss | 1
backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardRequest.java | 33
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutScheduler.java | 39
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.json | 10
backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalCreateRequest.java | 18
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalService.java | 236 ++++
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxml | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.js | 1
backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalServiceTest.java | 173 +++
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxss | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.json | 9
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.js | 1
backend/src/test/java/com/tourguide/modules/finance/service/GuideBalanceSettlementServiceTest.java | 66 +
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.js | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.json | 9
backend/db/migrations/20260817_100000_add_guide_balance_settlement.sql | 49
backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceServiceTest.java | 38
backend/db/backups/guide_withdrawal_before_payout_flow_20260817_115843.sql | 71 +
backend/db/backups/guide_bank_branch_before_20260814_171751.sql | 61 +
backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementService.java | 53
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxss | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.json | 10
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.js | 1
backend/db/migrations/20260814_161000_create_guide_bank_branch.sql | 35
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxss | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.js | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxml | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxml | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxss | 1
backend/db/backups/guide_withdrawal_before_status_reconcile_20260818_161625.sql | 80 +
backend/db/migrations/20260814_160000_integrate_guide_settlement.sql | 34
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.json | 10
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxml | 1
backend/src/main/java/com/tourguide/api/guide/GuideSettlementController.java | 73 +
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalStateService.java | 198 +++
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceService.java | 39
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.js | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxss | 1
backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankBranchResponse.java | 31
backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementScheduler.java | 39
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutService.java | 182 +++
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.js | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxml | 1
backend/src/main/java/com/tourguide/api/guide/GuideWithdrawalController.java | 51
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.js | 1
backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalStateServiceTest.java | 79 +
backend/src/main/java/com/tourguide/modules/finance/mapper/GuideBankBranchMapper.java | 7
backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementOnboardingRequest.java | 78 +
backend/src/main/java/com/tourguide/modules/finance/entity/GuideBankBranch.java | 42
backend/src/main/java/com/tourguide/modules/finance/service/GuideSettlementService.java | 513 +++++++++
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.json | 9
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxml | 1
backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutServiceTest.java | 151 ++
backend/db/migrations/20260817_110000_integrate_withdrawal_payout_flow.sql | 13
backend/db/backups/guide_balance_settlement_before_20260817_113100.sql | 240 ++++
backend/src/test/java/com/tourguide/common/huifu/biz/DouGongBasicDataBizTest.java | 37
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.json | 9
backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementStatusResponse.java | 23
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxml | 1
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxss | 1
backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardResponse.java | 37
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.json | 9
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxss | 1
backend/src/test/java/com/tourguide/modules/finance/service/GuideSettlementServiceTest.java | 262 ++++
mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxml | 1
66 files changed, 3,193 insertions(+), 0 deletions(-)
diff --git a/backend/db/backups/guide_balance_settlement_before_20260817_113100.sql b/backend/db/backups/guide_balance_settlement_before_20260817_113100.sql
new file mode 100644
index 0000000..6b66b6a
--- /dev/null
+++ b/backend/db/backups/guide_balance_settlement_before_20260817_113100.sql
@@ -0,0 +1,240 @@
+-- MySQL dump 10.13 Distrib 8.0.35, for Win64 (x86_64)
+--
+-- Host: 127.0.0.1 Database: tour_guide
+-- ------------------------------------------------------
+-- Server version 8.0.35
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `guide_user`
+--
+
+DROP TABLE IF EXISTS `guide_user`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `guide_user` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '导游用户主键',
+ `username` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游端登录用户名',
+ `password` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'BCrypt 加密后的登录密码',
+ `real_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游真实姓名',
+ `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游联系电话',
+ `avatar` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游头像地址',
+ `certificate_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游资格证编号',
+ `gender` tinyint DEFAULT NULL,
+ `birth_date` date DEFAULT NULL,
+ `work_years` int unsigned DEFAULT NULL,
+ `id_card_no` varchar(18) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `id_card_front_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `id_card_back_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `guide_certificate_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `qualification_certificate_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `team_id` bigint unsigned DEFAULT NULL,
+ `accepted_order_count` bigint unsigned NOT NULL DEFAULT '0',
+ `current_month_order_count` bigint unsigned NOT NULL DEFAULT '0',
+ `rating` decimal(3,2) NOT NULL DEFAULT '0.00',
+ `auto_disable_whitelisted` tinyint NOT NULL DEFAULT '0',
+ `wechat_open_id` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` tinyint NOT NULL DEFAULT '1' COMMENT '0 disabled, 1 enabled, 2 application pending',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_username` (`username`),
+ UNIQUE KEY `uk_guide_certificate_no` (`certificate_no`),
+ UNIQUE KEY `uk_guide_id_card_no` (`id_card_no`),
+ KEY `idx_guide_status` (`status`),
+ KEY `idx_guide_team_id` (`team_id`),
+ KEY `idx_guide_whitelist` (`auto_disable_whitelisted`),
+ KEY `idx_guide_rating` (`rating`),
+ CONSTRAINT `fk_guide_user_team` FOREIGN KEY (`team_id`) REFERENCES `guide_team` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游端用户表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `guide_user`
+--
+
+LOCK TABLES `guide_user` WRITE;
+/*!40000 ALTER TABLE `guide_user` DISABLE KEYS */;
+INSERT INTO `guide_user` VALUES (1,'1','1','1','1','1','1',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,0,0.00,0,NULL,1,0,'2026-07-27 16:08:50','2026-07-27 16:08:50'),(2,'123','$2a$10$9BYTbSNHhGwmmi4xEpyI7ORlWhj.IOk0CLGDOuXuqtiCppoNTh006','112','123',NULL,'1123',NULL,NULL,NULL,'1123','1','1',NULL,NULL,NULL,1,1,0.00,1,NULL,1,0,'2026-07-27 18:11:43','2026-07-29 11:36:18'),(4,'17777777777','$2a$10$N2lF5WhyENOnpAunr5XNB.u2qTPoXC75cJfAzM4DoyQHhkBUjKev6','1','17777777777',NULL,NULL,1,NULL,NULL,'511028200002150816','/uploads/guide-applications/38717f8f632f4f37a74c84a0c554e425.jpg','/uploads/guide-applications/f251aea159bf45ff8bcf3c9d179a1606.jpg','/uploads/guide-applications/f0278699b6914c1085fe9be9b4683e21.jpg',NULL,NULL,1,1,2.00,0,NULL,0,0,'2026-07-30 08:57:44','2026-08-13 15:02:52'),(5,'17777777778','$2a$10$FKKmIGXkO0t5xaG0cCnss.7T6KrGTx35GnQZiz3tdwyehdaQh0X5a','1','17777777778',NULL,NULL,1,NULL,NULL,'51090219960705552X','/uploads/guide-applications/3ae2a5ccd72348d38769f4aca40e357b.png','/uploads/guide-applications/1f1ec838c6e84e5bb1f292c3269da82a.png','/uploads/guide-applications/867f42c7581b4e4fa6324fd14eba28d5.png','/uploads/guide-applications/51f6a970b4dc4093bdf3600852661e2c.png',NULL,0,0,1.00,0,NULL,1,0,'2026-08-12 16:14:46','2026-08-14 17:46:10');
+/*!40000 ALTER TABLE `guide_user` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `tour_order`
+--
+
+DROP TABLE IF EXISTS `tour_order`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tour_order` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '订单主键',
+ `order_no` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务订单编号',
+ `visitor_id` bigint unsigned DEFAULT NULL COMMENT '游客用户ID',
+ `visitor_nickname` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '下单游客昵称快照',
+ `visitor_mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '下单游客手机号快照',
+ `scenic_spot_id` bigint unsigned DEFAULT NULL,
+ `scenic_name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '景区名称快照',
+ `meeting_point_id` bigint unsigned DEFAULT NULL,
+ `meeting_point_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '汇合点名称快照',
+ `package_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '套餐名称快照',
+ `service_package_id` bigint unsigned DEFAULT NULL,
+ `family_no` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '家庭号或同行组编号',
+ `order_type` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单类型:GROUP_TOUR拼团精讲、PRIVATE_TOUR私人定制',
+ `people_count` int unsigned NOT NULL DEFAULT '1' COMMENT '出行人数',
+ `unit_price` decimal(10,2) NOT NULL COMMENT '订单单价',
+ `original_price` decimal(10,2) DEFAULT NULL COMMENT '商品原价',
+ `discount_price` decimal(10,2) DEFAULT NULL COMMENT '商品优惠价',
+ `subtotal_amount` decimal(10,2) DEFAULT NULL COMMENT '商品小计金额',
+ `discount_type` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '优惠方式',
+ `discount_amount` decimal(10,2) DEFAULT NULL COMMENT '优惠金额',
+ `amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
+ `payment_method` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付方式:WECHAT、ALIPAY',
+ `payment_order_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '第三方支付流水号',
+ `payment_amount` decimal(10,2) DEFAULT NULL COMMENT '实际支付金额',
+ `payment_time` datetime DEFAULT NULL COMMENT '支付时间',
+ `placed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
+ `meeting_point_count` int unsigned NOT NULL DEFAULT '0' COMMENT '讲解路线汇合点数量',
+ `guide_id` bigint unsigned DEFAULT NULL COMMENT '接单导游ID',
+ `guide_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游姓名快照',
+ `guide_mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游手机号快照',
+ `receiving_session_id` bigint unsigned DEFAULT NULL,
+ `commission_source` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '抽成来源:PERSONAL个人平台配置、TEAM讲解队配置',
+ `commission_rate` decimal(5,2) DEFAULT NULL COMMENT '接单时平台抽成比例快照,单位百分比',
+ `commission_team_id` bigint unsigned DEFAULT NULL COMMENT '讲解队抽成来源ID,个人导游为空',
+ `guide_earning_amount` decimal(10,2) DEFAULT NULL COMMENT '导游实际业绩金额快照',
+ `accepted_at` datetime DEFAULT NULL COMMENT '导游接单时间',
+ `arrived_at` datetime DEFAULT NULL COMMENT '导游到达汇合点时间',
+ `service_started_at` datetime DEFAULT NULL COMMENT '讲解开始时间',
+ `completed_at` datetime DEFAULT NULL COMMENT '讲解结束时间',
+ `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单状态',
+ `refund_status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NONE' COMMENT '退款状态:NONE、REFUNDED',
+ `refund_amount` decimal(10,2) DEFAULT NULL COMMENT '退款金额',
+ `refund_time` datetime DEFAULT NULL COMMENT '退款时间',
+ `refund_order_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '退款流水号',
+ `refund_method` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '退款方式',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_tour_order_no` (`order_no`),
+ UNIQUE KEY `uk_tour_order_payment_order_no` (`payment_order_no`),
+ UNIQUE KEY `uk_tour_order_refund_order_no` (`refund_order_no`),
+ KEY `idx_tour_order_visitor` (`visitor_id`),
+ KEY `idx_tour_order_guide` (`guide_id`),
+ KEY `idx_tour_order_scenic` (`scenic_name`),
+ KEY `idx_tour_order_status` (`status`),
+ KEY `idx_tour_order_created_at` (`created_at`),
+ KEY `idx_tour_order_guide_earning` (`guide_id`,`status`,`completed_at`),
+ KEY `idx_tour_order_match` (`status`,`scenic_spot_id`,`meeting_point_id`,`order_type`),
+ KEY `idx_tour_order_session` (`receiving_session_id`),
+ CONSTRAINT `fk_tour_order_guide` FOREIGN KEY (`guide_id`) REFERENCES `guide_user` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `fk_tour_order_visitor` FOREIGN KEY (`visitor_id`) REFERENCES `mini_user` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游客讲解服务订单表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `tour_order`
+--
+
+LOCK TABLES `tour_order` WRITE;
+/*!40000 ALTER TABLE `tour_order` DISABLE KEYS */;
+INSERT INTO `tour_order` VALUES (1,'MJ2026072911230118995385',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 快讲',5,'1','GROUP_QUICK',1,0.11,0.11,0.11,0.11,'NONE',0.00,0.11,'WECHAT','LOCALMJ2026072911230118995385',0.11,'2026-07-29 11:23:01','2026-07-29 11:23:01',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','REFUNDED',0.11,'2026-07-29 11:31:30','RFMJ20260729113129956AD2DA','WECHAT','2026-07-29 11:23:01','2026-08-04 09:43:54',0),(2,'MJ202607291132100509EF6E',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.02,0.02,0.02,0.02,'NONE',0.00,0.02,'WECHAT','LOCALMJ202607291132100509EF6E',0.02,'2026-07-29 11:32:15','2026-07-29 11:32:10',1,2,'112','123',NULL,'PERSONAL',15.00,NULL,0.02,'2026-07-29 11:36:18',NULL,NULL,NULL,'CANCELLED','REFUNDED',0.02,'2026-07-30 10:49:28','RFMJ20260730104927925EC210','WECHAT','2026-07-29 11:32:10','2026-08-04 09:43:54',0),(3,'MJ202607291135271504313A',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 快讲',5,'1','GROUP_QUICK',1,0.11,0.11,0.11,0.11,'NONE',0.00,0.11,'WECHAT','LOCALMJ202607291135271504313A',0.11,'2026-07-29 11:35:31','2026-07-29 11:35:27',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','REFUNDED',0.11,'2026-07-29 17:15:55','LOCALRF202607291715549C6D60','WECHAT','2026-07-29 11:35:27','2026-08-04 09:43:54',0),(4,'MJ20260730104932284B29FF',2,'微信用户','17828262728',1,'上海外滩',1,'外滩观景平台','拼团 · 快讲',1,'1','GROUP_QUICK',1,36.00,40.00,36.00,36.00,'SCENIC_PRICE',4.00,36.00,'WECHAT','LOCALMJ20260730104932284B29FF',36.00,'2026-07-30 10:49:36','2026-07-30 10:49:32',1,4,'1','17777777777',1,'PERSONAL',15.00,NULL,30.60,'2026-07-30 10:49:36',NULL,'2026-07-30 11:04:43','2026-07-30 11:55:15','REFUNDED','REFUNDED',36.00,'2026-08-13 15:25:10','RF20260813152509F90496','WECHAT','2026-07-30 10:49:32','2026-08-04 09:43:54',0),(5,'MJ20260803165309194181BC',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','DGMJ20260803165309194181BC',NULL,NULL,'2026-08-03 16:53:09',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','NONE',NULL,NULL,NULL,NULL,'2026-08-03 16:53:09','2026-08-04 09:43:55',0),(6,'MJ202608031709358580158B',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','DGMJ202608031709358580158B',NULL,NULL,'2026-08-03 17:09:36',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','NONE',NULL,NULL,NULL,NULL,'2026-08-03 17:09:36','2026-08-04 09:43:55',0),(7,'MJ2026080317141856030A6A',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','DGMJ2026080317141856030A6A',NULL,NULL,'2026-08-03 17:14:19',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','NONE',NULL,NULL,NULL,NULL,'2026-08-03 17:14:19','2026-08-04 09:43:55',0),(8,'MJ20260803173412219CF48E',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','DGMJ20260803173412219CF48E',NULL,NULL,'2026-08-03 17:34:12',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','NONE',NULL,NULL,NULL,NULL,'2026-08-03 17:34:12','2026-08-04 09:43:55',0),(9,'MJ20260803173900195A0F99',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','DGMJ20260803173900195A0F99',NULL,NULL,'2026-08-03 17:39:00',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','NONE',NULL,NULL,NULL,NULL,'2026-08-03 17:39:00','2026-08-04 09:43:55',0),(10,'MJ202608031754367295A3F0',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','PY202608031754384660000000000001',0.01,'2026-08-03 17:55:43','2026-08-03 17:54:37',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','REFUNDED',0.01,'2026-08-03 17:56:55','RFMJ202608031756547857067B','WECHAT','2026-08-03 17:54:37','2026-08-04 09:42:59',0),(11,'MJ20260804085554569FE14D',2,'微信用户','17828262728',2,'豫园',3,'豫园正门','拼团 · 精讲',6,'1','GROUP_PREMIUM',1,0.01,0.01,0.01,0.01,'NONE',0.00,0.01,'WECHAT','PY202608040855572570000000000001',0.01,'2026-08-04 08:56:11','2026-08-04 08:55:55',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'CANCELLED','REFUNDED',0.01,'2026-08-04 08:56:36','RF202608040856311100000000000002','WECHAT','2026-08-04 08:55:55','2026-08-04 09:43:55',0);
+/*!40000 ALTER TABLE `tour_order` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `guide_withdrawal`
+--
+
+DROP TABLE IF EXISTS `guide_withdrawal`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `guide_withdrawal` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '提现申请主键',
+ `withdrawal_no` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提现业务编号',
+ `guide_id` bigint unsigned NOT NULL COMMENT '申请导游主键',
+ `guide_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游姓名快照',
+ `guide_mobile` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游手机号快照',
+ `amount` decimal(10,2) NOT NULL COMMENT '提现金额',
+ `bank_card_id` int DEFAULT NULL COMMENT '提现结算卡账户主键',
+ `bank_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款银行快照',
+ `card_masked` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款卡号掩码',
+ `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING、APPROVED、REJECTED',
+ `audit_remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '处理备注',
+ `processor` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '后台处理人',
+ `applied_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
+ `processed_at` datetime DEFAULT NULL COMMENT '处理时间',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_withdrawal_no` (`withdrawal_no`),
+ KEY `idx_guide_withdrawal_guide` (`guide_id`),
+ KEY `idx_guide_withdrawal_status` (`status`),
+ KEY `idx_guide_withdrawal_applied_at` (`applied_at`),
+ KEY `idx_guide_withdrawal_guide_status` (`guide_id`,`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游提现申请表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `guide_withdrawal`
+--
+
+LOCK TABLES `guide_withdrawal` WRITE;
+/*!40000 ALTER TABLE `guide_withdrawal` DISABLE KEYS */;
+/*!40000 ALTER TABLE `guide_withdrawal` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `sys_config`
+--
+
+DROP TABLE IF EXISTS `sys_config`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sys_config` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '参数配置主键',
+ `config_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数名称',
+ `config_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数键名',
+ `config_value` varchar(1000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数键值',
+ `config_type` tinyint NOT NULL DEFAULT '0' COMMENT '是否系统内置:0否,1是',
+ `remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数说明',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_config_key` (`config_key`)
+) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台参数配置表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `sys_config`
+--
+
+LOCK TABLES `sys_config` WRITE;
+/*!40000 ALTER TABLE `sys_config` DISABLE KEYS */;
+INSERT INTO `sys_config` VALUES (1,'后台系统名称','system.admin.title','TourGuide管理后台',1,'后台页面显示的系统名称','2026-07-27 11:05:18','2026-07-27 11:05:18',0),(2,'默认分页大小','system.page.default-size','10',1,'后台列表默认每页记录数','2026-07-27 11:05:18','2026-07-27 11:05:18',0),(4,'未接单自动退款时间','platform.order.unaccepted-auto-refund-minutes','15',1,'单位:分钟','2026-07-28 14:50:43','2026-07-28 14:50:43',0),(5,'个人导游提成比例','platform.guide.commission-rate','15',1,'单位:百分比','2026-07-28 14:50:43','2026-07-28 14:50:43',0),(6,'精准可用最低接单数','platform.dispatch.precise-order-count','10',1,'达到该接单数后参与精准派单','2026-07-28 14:50:43','2026-07-28 14:50:43',0),(7,'精准可用最低综合评分','platform.dispatch.precise-rating','4.8',1,'达到该评分后参与精准派单','2026-07-28 14:50:43','2026-07-28 14:50:43',0),(8,'订单结算周期','platform.finance.settlement-days','1',1,'T+N天结算','2026-07-28 14:50:43','2026-07-28 14:50:43',0),(9,'客服微信','platform.customer-service.wechat','TourGuide客服1',1,'平台客服微信号','2026-07-28 14:50:43','2026-07-29 11:20:24',0),(10,'客服电话','platform.customer-service.phone','400-000-0001',1,'平台客服电话','2026-07-28 14:50:43','2026-07-29 11:20:22',0);
+/*!40000 ALTER TABLE `sys_config` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2026-08-17 11:30:54
diff --git a/backend/db/backups/guide_bank_branch_before_20260814_171751.sql b/backend/db/backups/guide_bank_branch_before_20260814_171751.sql
new file mode 100644
index 0000000..427e0bf
--- /dev/null
+++ b/backend/db/backups/guide_bank_branch_before_20260814_171751.sql
@@ -0,0 +1,61 @@
+-- MySQL dump 10.13 Distrib 8.0.35, for Win64 (x86_64)
+--
+-- Host: 127.0.0.1 Database: tour_guide
+-- ------------------------------------------------------
+-- Server version 8.0.35
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `guide_bank_branch`
+--
+
+DROP TABLE IF EXISTS `guide_bank_branch`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `guide_bank_branch` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '开户行主键',
+ `bank_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '银行名称',
+ `branch_name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '开户行名称',
+ `branch_code` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联行号',
+ `city_code` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '城市行政区划编码',
+ `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1启用,0停用',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0正常,1删除',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_bank_branch_code` (`branch_code`),
+ KEY `idx_guide_bank_branch_city_status` (`city_code`,`status`),
+ KEY `idx_guide_bank_branch_name` (`bank_name`,`branch_name`)
+) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游结算卡开户行基础表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `guide_bank_branch`
+--
+
+LOCK TABLES `guide_bank_branch` WRITE;
+/*!40000 ALTER TABLE `guide_bank_branch` DISABLE KEYS */;
+INSERT INTO `guide_bank_branch` VALUES (1,'招商银行','招商银行成都高新支行','CMB51010001','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(2,'招商银行','招商银行都江堰支行','CMB51010002','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(3,'中国银行','中国银行成都天府支行','BOC51010001','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(4,'中国银行','中国银行都江堰支行','BOC51010002','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(5,'工商银行','工商银行成都春熙路支行','ICBC51010001','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(6,'工商银行','工商银行青城山支行','ICBC51010002','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(7,'建设银行','建设银行成都高新支行','CCB51010001','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02'),(8,'农业银行','农业银行都江堰支行','ABC51010001','510100',1,0,'2026-08-14 16:14:02','2026-08-14 16:14:02');
+/*!40000 ALTER TABLE `guide_bank_branch` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2026-08-14 17:17:51
diff --git a/backend/db/backups/guide_withdrawal_before_payout_flow_20260817_115843.sql b/backend/db/backups/guide_withdrawal_before_payout_flow_20260817_115843.sql
new file mode 100644
index 0000000..ecba292
--- /dev/null
+++ b/backend/db/backups/guide_withdrawal_before_payout_flow_20260817_115843.sql
@@ -0,0 +1,71 @@
+-- MySQL dump 10.13 Distrib 8.0.35, for Win64 (x86_64)
+--
+-- Host: 127.0.0.1 Database: tour_guide
+-- ------------------------------------------------------
+-- Server version 8.0.35
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `guide_withdrawal`
+--
+
+DROP TABLE IF EXISTS `guide_withdrawal`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `guide_withdrawal` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '提现申请主键',
+ `withdrawal_no` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提现业务编号',
+ `guide_id` bigint unsigned NOT NULL COMMENT '申请导游主键',
+ `guide_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游姓名快照',
+ `guide_mobile` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游手机号快照',
+ `amount` decimal(10,2) NOT NULL COMMENT '提现金额',
+ `bank_card_id` int DEFAULT NULL COMMENT '提现结算卡账户主键',
+ `bank_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款银行快照',
+ `card_masked` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款卡号掩码',
+ `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING、APPROVED、REJECTED',
+ `audit_remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '处理备注',
+ `processor` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '后台处理人',
+ `applied_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
+ `processed_at` datetime DEFAULT NULL COMMENT '处理时间',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_withdrawal_no` (`withdrawal_no`),
+ KEY `idx_guide_withdrawal_guide` (`guide_id`),
+ KEY `idx_guide_withdrawal_status` (`status`),
+ KEY `idx_guide_withdrawal_applied_at` (`applied_at`),
+ KEY `idx_guide_withdrawal_guide_status` (`guide_id`,`status`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游提现申请表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `guide_withdrawal`
+--
+
+LOCK TABLES `guide_withdrawal` WRITE;
+/*!40000 ALTER TABLE `guide_withdrawal` DISABLE KEYS */;
+INSERT INTO `guide_withdrawal` VALUES (1,'WD202608171135372460721',5,'1','17777777778',0.01,590,'中国工商银行','****1378','REJECTED','1','com.tourguide.security.AppUserPrincipal@281fa12f','2026-08-17 11:35:37','2026-08-17 11:37:14','2026-08-17 11:35:37','2026-08-17 11:35:37',0),(2,'WD202608171137359953259',5,'1','17777777778',0.01,590,'中国工商银行','****1378','PENDING',NULL,NULL,'2026-08-17 11:37:36',NULL,'2026-08-17 11:37:36','2026-08-17 11:37:36',0);
+/*!40000 ALTER TABLE `guide_withdrawal` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2026-08-17 11:58:43
diff --git a/backend/db/backups/guide_withdrawal_before_status_reconcile_20260818_161625.sql b/backend/db/backups/guide_withdrawal_before_status_reconcile_20260818_161625.sql
new file mode 100644
index 0000000..b4f42e9
--- /dev/null
+++ b/backend/db/backups/guide_withdrawal_before_status_reconcile_20260818_161625.sql
@@ -0,0 +1,80 @@
+-- MySQL dump 10.13 Distrib 8.0.35, for Win64 (x86_64)
+--
+-- Host: 127.0.0.1 Database: tour_guide
+-- ------------------------------------------------------
+-- Server version 8.0.35
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `guide_withdrawal`
+--
+
+DROP TABLE IF EXISTS `guide_withdrawal`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `guide_withdrawal` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '提现申请主键',
+ `withdrawal_no` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提现业务编号',
+ `guide_id` bigint unsigned NOT NULL COMMENT '申请导游主键',
+ `guide_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游姓名快照',
+ `guide_mobile` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '导游手机号快照',
+ `amount` decimal(10,2) NOT NULL COMMENT '提现金额',
+ `bank_card_id` int DEFAULT NULL COMMENT '提现结算卡账户主键',
+ `bank_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款银行快照',
+ `card_masked` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收款卡号掩码',
+ `transfer_req_seq_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '主账户划账请求流水号',
+ `transfer_req_date` char(8) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '主账户划账请求日期',
+ `transfer_hf_seq_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '主账户划账汇付全局流水号',
+ `transferred_at` datetime DEFAULT NULL COMMENT '主账户划账完成时间',
+ `cash_req_seq_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游账户取现请求流水号',
+ `cash_req_date` char(8) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游账户取现请求日期',
+ `cash_hf_seq_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导游账户取现汇付全局流水号',
+ `payout_message` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '资金处理状态说明',
+ `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING、APPROVED、REJECTED',
+ `audit_remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '处理备注',
+ `processor` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '后台处理人',
+ `applied_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
+ `processed_at` datetime DEFAULT NULL COMMENT '处理时间',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常,1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_withdrawal_no` (`withdrawal_no`),
+ KEY `idx_guide_withdrawal_guide` (`guide_id`),
+ KEY `idx_guide_withdrawal_status` (`status`),
+ KEY `idx_guide_withdrawal_applied_at` (`applied_at`),
+ KEY `idx_guide_withdrawal_guide_status` (`guide_id`,`status`),
+ KEY `idx_guide_withdrawal_payout` (`status`,`updated_at`)
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游提现申请表';
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `guide_withdrawal`
+--
+
+LOCK TABLES `guide_withdrawal` WRITE;
+/*!40000 ALTER TABLE `guide_withdrawal` DISABLE KEYS */;
+INSERT INTO `guide_withdrawal` VALUES (1,'WD202608171135372460721',5,'1','17777777778',0.01,590,'中国工商银行','****1378',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'REJECTED','1','com.tourguide.security.AppUserPrincipal@281fa12f','2026-08-17 11:35:37','2026-08-17 11:37:14','2026-08-17 11:35:37','2026-08-17 11:35:37',0),(2,'WD202608171137359953259',5,'1','17777777778',0.01,590,'中国工商银行','****1378',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'PENDING',NULL,NULL,'2026-08-17 11:37:36',NULL,'2026-08-17 11:37:36','2026-08-17 11:37:36',0),(3,'WD202608171351110092379',5,'1','17777777778',0.01,590,'中国工商银行','****1378','TF202608171351223475738','20260817','003500TOP2A260817135125P628ac139d1d00000','2026-08-17 13:51:23','CW202608181610034982111','20260818','003000TOP3A260818161008P166ac139c5300000','导游账户取现失败:等待系统重新发起取现','TRANSFERRED','1','com.tourguide.security.AppUserPrincipal@342b0eac','2026-08-17 13:51:11','2026-08-18 16:10:04','2026-08-17 13:51:11','2026-08-17 13:51:11',0),(4,'WD202608171404136365335',5,'1','17777777778',0.01,590,'中国工商银行','****1378','TF202608171404173990647','20260817','003500TOP2B260817140420P172ac139c7500000','2026-08-17 14:04:18','CW202608181610040336994','20260818','003000TOP1A260818161008P795ac139d5100000','导游账户取现失败:等待系统重新发起取现','TRANSFERRED',NULL,'com.tourguide.security.AppUserPrincipal@5a157506','2026-08-17 14:04:14','2026-08-18 16:10:05','2026-08-17 14:04:14','2026-08-17 14:04:14',0),(5,'WD202608171406268259691',5,'1','17777777778',0.01,590,'中国工商银行','****1378','TF202608171406380201633','20260817','003500TOP3B260817140640P835ac13684700000','2026-08-17 14:06:39','CW202608181610046915899','20260818','003000TOP1A260818161009P490ac139c3c00000','导游账户取现失败:等待系统重新发起取现','TRANSFERRED',NULL,'com.tourguide.security.AppUserPrincipal@32c318d5','2026-08-17 14:06:27','2026-08-18 16:10:05','2026-08-17 14:06:27','2026-08-17 14:06:27',0);
+/*!40000 ALTER TABLE `guide_withdrawal` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2026-08-18 16:16:25
diff --git a/backend/db/migrations/20260814_160000_integrate_guide_settlement.sql b/backend/db/migrations/20260814_160000_integrate_guide_settlement.sql
new file mode 100644
index 0000000..5e067eb
--- /dev/null
+++ b/backend/db/migrations/20260814_160000_integrate_guide_settlement.sql
@@ -0,0 +1,34 @@
+USE `tour_guide`;
+
+-- 目的:持久化导游端个人/企业进件资料、结算卡审核状态以及提现收款卡快照。
+ALTER TABLE `t_user_withdrawal`
+ ADD COLUMN `subjectType` VARCHAR(16) DEFAULT NULL COMMENT '进件主体:individual、enterprise' AFTER `userType`,
+ ADD COLUMN `enterpriseName` VARCHAR(128) DEFAULT NULL COMMENT '企业名称' AFTER `subjectType`,
+ ADD COLUMN `creditCode` VARCHAR(18) DEFAULT NULL COMMENT '统一社会信用代码' AFTER `enterpriseName`,
+ ADD COLUMN `licenseValidityType` VARCHAR(16) DEFAULT NULL COMMENT '营业执照期限:fixed、long_term' AFTER `creditCode`,
+ ADD COLUMN `licenseBeginDate` VARCHAR(10) DEFAULT NULL COMMENT '营业执照开始日期' AFTER `licenseValidityType`,
+ ADD COLUMN `licenseEndDate` VARCHAR(10) DEFAULT NULL COMMENT '营业执照结束日期' AFTER `licenseBeginDate`,
+ ADD COLUMN `registeredProvinceCode` VARCHAR(16) DEFAULT NULL COMMENT '注册地址省编码' AFTER `licenseEndDate`,
+ ADD COLUMN `registeredCityCode` VARCHAR(16) DEFAULT NULL COMMENT '注册地址市编码' AFTER `registeredProvinceCode`,
+ ADD COLUMN `registeredDistrictCode` VARCHAR(16) DEFAULT NULL COMMENT '注册地址区县编码' AFTER `registeredCityCode`,
+ ADD COLUMN `registeredAddress` VARCHAR(255) DEFAULT NULL COMMENT '详细注册地址' AFTER `registeredDistrictCode`,
+ ADD COLUMN `contactName` VARCHAR(64) DEFAULT NULL COMMENT '联系人姓名' AFTER `registeredAddress`,
+ ADD COLUMN `bankBranchName` VARCHAR(128) DEFAULT NULL COMMENT '开户行名称' AFTER `bankBranchCode`,
+ ADD COLUMN `bankCardStatus` VARCHAR(16) DEFAULT NULL COMMENT '结算卡状态:local、pending、approved、rejected' AFTER `tokenNo`,
+ ADD COLUMN `bankCardReason` VARCHAR(255) DEFAULT NULL COMMENT '结算卡失败原因' AFTER `bankCardStatus`,
+ ADD KEY `idx_withdrawal_user_active` (`userId`, `userType`, `type`, `status`);
+
+UPDATE `t_user_withdrawal`
+SET `subjectType` = COALESCE(`subjectType`, 'individual'),
+ `bankCardStatus` = CASE
+ WHEN `tokenNo` IS NOT NULL AND `tokenNo` <> '' THEN 'approved'
+ WHEN `bankCardCode` IS NOT NULL AND `bankCardCode` <> '' THEN 'pending'
+ ELSE NULL
+ END
+WHERE `userType` = 2 AND `type` = 1;
+
+ALTER TABLE `guide_withdrawal`
+ ADD COLUMN `bank_card_id` INT DEFAULT NULL COMMENT '提现结算卡账户主键' AFTER `amount`,
+ ADD COLUMN `bank_name` VARCHAR(64) DEFAULT NULL COMMENT '收款银行快照' AFTER `bank_card_id`,
+ ADD COLUMN `card_masked` VARCHAR(32) DEFAULT NULL COMMENT '收款卡号掩码' AFTER `bank_name`,
+ ADD KEY `idx_guide_withdrawal_guide_status` (`guide_id`, `status`);
diff --git a/backend/db/migrations/20260814_161000_create_guide_bank_branch.sql b/backend/db/migrations/20260814_161000_create_guide_bank_branch.sql
new file mode 100644
index 0000000..aab22ce
--- /dev/null
+++ b/backend/db/migrations/20260814_161000_create_guide_bank_branch.sql
@@ -0,0 +1,35 @@
+USE `tour_guide`;
+
+-- 目的:把导游绑定结算卡使用的开户行数据从应用代码迁移为可维护基础数据。
+CREATE TABLE IF NOT EXISTS `guide_bank_branch` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '开户行主键',
+ `bank_name` VARCHAR(64) NOT NULL COMMENT '银行名称',
+ `branch_name` VARCHAR(128) NOT NULL COMMENT '开户行名称',
+ `branch_code` VARCHAR(32) NOT NULL COMMENT '联行号',
+ `city_code` VARCHAR(16) NOT NULL COMMENT '城市行政区划编码',
+ `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0停用',
+ `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常,1删除',
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_guide_bank_branch_code` (`branch_code`),
+ KEY `idx_guide_bank_branch_city_status` (`city_code`, `status`),
+ KEY `idx_guide_bank_branch_name` (`bank_name`, `branch_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导游结算卡开户行基础表';
+
+INSERT INTO `guide_bank_branch` (`bank_name`, `branch_name`, `branch_code`, `city_code`, `status`)
+VALUES
+ ('招商银行', '招商银行成都高新支行', 'CMB51010001', '510100', 1),
+ ('招商银行', '招商银行都江堰支行', 'CMB51010002', '510100', 1),
+ ('中国银行', '中国银行成都天府支行', 'BOC51010001', '510100', 1),
+ ('中国银行', '中国银行都江堰支行', 'BOC51010002', '510100', 1),
+ ('工商银行', '工商银行成都春熙路支行', 'ICBC51010001', '510100', 1),
+ ('工商银行', '工商银行青城山支行', 'ICBC51010002', '510100', 1),
+ ('建设银行', '建设银行成都高新支行', 'CCB51010001', '510100', 1),
+ ('农业银行', '农业银行都江堰支行', 'ABC51010001', '510100', 1)
+ON DUPLICATE KEY UPDATE
+ `bank_name` = VALUES(`bank_name`),
+ `branch_name` = VALUES(`branch_name`),
+ `city_code` = VALUES(`city_code`),
+ `status` = VALUES(`status`),
+ `deleted` = 0;
diff --git a/backend/db/migrations/20260817_100000_add_guide_balance_settlement.sql b/backend/db/migrations/20260817_100000_add_guide_balance_settlement.sql
new file mode 100644
index 0000000..918f79f
--- /dev/null
+++ b/backend/db/migrations/20260817_100000_add_guide_balance_settlement.sql
@@ -0,0 +1,49 @@
+USE `tour_guide`;
+
+-- 目的:按订单创建时的结算周期,在服务完成并到期后将导游收益幂等计入可提现余额。
+ALTER TABLE `guide_user`
+ ADD COLUMN `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '导游可提现余额' AFTER `wechat_open_id`;
+
+ALTER TABLE `tour_order`
+ ADD COLUMN `settlement_days` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '下单时结算周期快照,单位天' AFTER `guide_earning_amount`,
+ ADD COLUMN `settlement_due_at` DATETIME DEFAULT NULL COMMENT '预计可分账时间' AFTER `settlement_days`,
+ ADD COLUMN `settled_at` DATETIME DEFAULT NULL COMMENT '实际分账入余额时间' AFTER `settlement_due_at`,
+ ADD KEY `idx_tour_order_settlement` (`status`, `settled_at`, `settlement_due_at`);
+
+-- 历史订单没有周期快照,统一使用迁移时的平台配置。
+UPDATE `tour_order`
+SET `settlement_days` = COALESCE((
+ SELECT CAST(`config_value` AS UNSIGNED)
+ FROM `sys_config`
+ WHERE `config_key` = 'platform.finance.settlement-days' AND `deleted` = 0
+ LIMIT 1
+), 1);
+
+-- 旧逻辑下已完成订单已经可提现,迁移时标记为已结算,防止定时任务重复入账。
+UPDATE `tour_order`
+SET `settlement_due_at` = TIMESTAMPADD(DAY, `settlement_days`, `completed_at`),
+ `settled_at` = NOW()
+WHERE `status` = 'COMPLETED' AND `completed_at` IS NOT NULL;
+
+-- 保持迁移前的可提现金额口径:已完成未退款收益减去所有未驳回提现。
+UPDATE `guide_user` AS `guide`
+LEFT JOIN (
+ SELECT `guide_id`, SUM(`guide_earning_amount`) AS `earning_amount`
+ FROM `tour_order`
+ WHERE `deleted` = 0
+ AND `status` = 'COMPLETED'
+ AND `guide_earning_amount` IS NOT NULL
+ AND (`refund_status` IS NULL OR `refund_status` <> 'REFUNDED')
+ GROUP BY `guide_id`
+) AS `earning` ON `earning`.`guide_id` = `guide`.`id`
+LEFT JOIN (
+ SELECT `guide_id`, SUM(`amount`) AS `withdrawal_amount`
+ FROM `guide_withdrawal`
+ WHERE `deleted` = 0 AND `status` IN ('PENDING', 'PROCESSING', 'APPROVED')
+ GROUP BY `guide_id`
+) AS `withdrawal` ON `withdrawal`.`guide_id` = `guide`.`id`
+SET `guide`.`balance` = GREATEST(
+ COALESCE(`earning`.`earning_amount`, 0) - COALESCE(`withdrawal`.`withdrawal_amount`, 0),
+ 0
+)
+WHERE `guide`.`deleted` = 0;
diff --git a/backend/db/migrations/20260817_110000_integrate_withdrawal_payout_flow.sql b/backend/db/migrations/20260817_110000_integrate_withdrawal_payout_flow.sql
new file mode 100644
index 0000000..dc835fb
--- /dev/null
+++ b/backend/db/migrations/20260817_110000_integrate_withdrawal_payout_flow.sql
@@ -0,0 +1,13 @@
+USE `tour_guide`;
+
+-- 目的:持久化“平台主账户划账到导游账户,再从导游账户取现”的资金阶段和幂等流水。
+ALTER TABLE `guide_withdrawal`
+ ADD COLUMN `transfer_req_seq_id` VARCHAR(32) DEFAULT NULL COMMENT '主账户划账请求流水号' AFTER `card_masked`,
+ ADD COLUMN `transfer_req_date` CHAR(8) DEFAULT NULL COMMENT '主账户划账请求日期' AFTER `transfer_req_seq_id`,
+ ADD COLUMN `transfer_hf_seq_id` VARCHAR(64) DEFAULT NULL COMMENT '主账户划账汇付全局流水号' AFTER `transfer_req_date`,
+ ADD COLUMN `transferred_at` DATETIME DEFAULT NULL COMMENT '主账户划账完成时间' AFTER `transfer_hf_seq_id`,
+ ADD COLUMN `cash_req_seq_id` VARCHAR(32) DEFAULT NULL COMMENT '导游账户取现请求流水号' AFTER `transferred_at`,
+ ADD COLUMN `cash_req_date` CHAR(8) DEFAULT NULL COMMENT '导游账户取现请求日期' AFTER `cash_req_seq_id`,
+ ADD COLUMN `cash_hf_seq_id` VARCHAR(64) DEFAULT NULL COMMENT '导游账户取现汇付全局流水号' AFTER `cash_req_date`,
+ ADD COLUMN `payout_message` VARCHAR(255) DEFAULT NULL COMMENT '资金处理状态说明' AFTER `cash_hf_seq_id`,
+ ADD KEY `idx_guide_withdrawal_payout` (`status`, `updated_at`);
diff --git a/backend/src/main/java/com/tourguide/api/guide/GuideSettlementController.java b/backend/src/main/java/com/tourguide/api/guide/GuideSettlementController.java
new file mode 100644
index 0000000..3cd3483
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/api/guide/GuideSettlementController.java
@@ -0,0 +1,73 @@
+package com.tourguide.api.guide;
+
+import com.tourguide.common.api.ApiResponse;
+import com.tourguide.modules.finance.dto.GuideBankBranchResponse;
+import com.tourguide.modules.finance.dto.GuideBankCardRequest;
+import com.tourguide.modules.finance.dto.GuideBankCardResponse;
+import com.tourguide.modules.finance.dto.GuideSettlementOnboardingRequest;
+import com.tourguide.modules.finance.dto.GuideSettlementStatusResponse;
+import com.tourguide.modules.finance.service.GuideSettlementService;
+import com.tourguide.security.AppUserPrincipal;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@Validated
+@RestController
+@RequestMapping("/api/guide/settlement")
+public class GuideSettlementController {
+ private final GuideSettlementService settlementService;
+
+ public GuideSettlementController(GuideSettlementService settlementService) {
+ this.settlementService = settlementService;
+ }
+
+ @GetMapping("/onboarding/status")
+ public ApiResponse<GuideSettlementStatusResponse> status(
+ @AuthenticationPrincipal AppUserPrincipal principal) {
+ return ApiResponse.success(settlementService.status(principal.getUserId()));
+ }
+
+ @PostMapping("/onboarding")
+ public ApiResponse<GuideSettlementStatusResponse> submit(
+ @AuthenticationPrincipal AppUserPrincipal principal,
+ @Valid @RequestBody GuideSettlementOnboardingRequest request) {
+ return ApiResponse.success(settlementService.submit(principal.getUserId(), request));
+ }
+
+ @GetMapping("/bank-cards")
+ public ApiResponse<List<GuideBankCardResponse>> bankCards(
+ @AuthenticationPrincipal AppUserPrincipal principal) {
+ return ApiResponse.success(settlementService.bankCards(principal.getUserId()));
+ }
+
+ @PostMapping("/bank-cards")
+ public ApiResponse<GuideBankCardResponse> bindBankCard(
+ @AuthenticationPrincipal AppUserPrincipal principal,
+ @Valid @RequestBody GuideBankCardRequest request) {
+ return ApiResponse.success(settlementService.bindBankCard(principal.getUserId(), request));
+ }
+
+ @PostMapping("/bank-cards/{cardId}/business-open")
+ public ApiResponse<GuideBankCardResponse> openBankCardBusiness(
+ @AuthenticationPrincipal AppUserPrincipal principal,
+ @PathVariable Integer cardId) {
+ return ApiResponse.success(settlementService.openBankCardBusiness(principal.getUserId(), cardId));
+ }
+
+ @GetMapping("/bank-branches")
+ public ApiResponse<List<GuideBankBranchResponse>> branches(
+ @RequestParam(required = false) String cityCode,
+ @RequestParam(required = false) String keyword) {
+ return ApiResponse.success(settlementService.branches(cityCode, keyword));
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/api/guide/GuideWithdrawalController.java b/backend/src/main/java/com/tourguide/api/guide/GuideWithdrawalController.java
new file mode 100644
index 0000000..0ab7ea4
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/api/guide/GuideWithdrawalController.java
@@ -0,0 +1,51 @@
+package com.tourguide.api.guide;
+
+import com.tourguide.common.api.ApiResponse;
+import com.tourguide.common.api.PageResponse;
+import com.tourguide.modules.finance.dto.GuideWithdrawalCreateRequest;
+import com.tourguide.modules.finance.dto.GuideWithdrawalSummaryResponse;
+import com.tourguide.modules.finance.dto.WithdrawalResponse;
+import com.tourguide.modules.finance.service.GuideWithdrawalService;
+import com.tourguide.security.AppUserPrincipal;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@Validated
+@RestController
+@RequestMapping("/api/guide/withdrawals")
+public class GuideWithdrawalController {
+ private final GuideWithdrawalService withdrawalService;
+
+ public GuideWithdrawalController(GuideWithdrawalService withdrawalService) {
+ this.withdrawalService = withdrawalService;
+ }
+
+ @GetMapping("/summary")
+ public ApiResponse<GuideWithdrawalSummaryResponse> summary(
+ @AuthenticationPrincipal AppUserPrincipal principal) {
+ return ApiResponse.success(withdrawalService.summary(principal.getUserId()));
+ }
+
+ @GetMapping
+ public ApiResponse<PageResponse<WithdrawalResponse>> page(
+ @AuthenticationPrincipal AppUserPrincipal principal,
+ @RequestParam(defaultValue = "1") long page,
+ @RequestParam(defaultValue = "50") long size) {
+ return ApiResponse.success(withdrawalService.page(principal.getUserId(), page, size));
+ }
+
+ @PostMapping
+ public ApiResponse<WithdrawalResponse> create(
+ @AuthenticationPrincipal AppUserPrincipal principal,
+ @Valid @RequestBody GuideWithdrawalCreateRequest request) {
+ return ApiResponse.success(withdrawalService.create(principal.getUserId(), request));
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankBranchResponse.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankBranchResponse.java
new file mode 100644
index 0000000..dc9c941
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankBranchResponse.java
@@ -0,0 +1,31 @@
+package com.tourguide.modules.finance.dto;
+
+import com.tourguide.modules.finance.entity.GuideBankBranch;
+
+public class GuideBankBranchResponse {
+ private final String id;
+ private final String bankName;
+ private final String branchName;
+ private final String branchCode;
+ private final String cityCode;
+
+ public GuideBankBranchResponse(String id, String bankName, String branchName,
+ String branchCode, String cityCode) {
+ this.id = id;
+ this.bankName = bankName;
+ this.branchName = branchName;
+ this.branchCode = branchCode;
+ this.cityCode = cityCode;
+ }
+
+ public GuideBankBranchResponse(GuideBankBranch branch) {
+ this(String.valueOf(branch.getId()), branch.getBankName(), branch.getBranchName(),
+ branch.getBranchCode(), branch.getCityCode());
+ }
+
+ public String getId() { return id; }
+ public String getBankName() { return bankName; }
+ public String getBranchName() { return branchName; }
+ public String getBranchCode() { return branchCode; }
+ public String getCityCode() { return cityCode; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardRequest.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardRequest.java
new file mode 100644
index 0000000..a2ed786
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardRequest.java
@@ -0,0 +1,33 @@
+package com.tourguide.modules.finance.dto;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+public class GuideBankCardRequest {
+ @NotBlank(message = "银行卡号不能为空")
+ @Pattern(regexp = "^\\d{16,32}$", message = "银行卡号格式不正确")
+ private String cardNumber;
+ @NotBlank(message = "开户省编码不能为空")
+ private String provinceCode;
+ @NotBlank(message = "开户城市编码不能为空")
+ private String cityCode;
+ @NotBlank(message = "银行名称不能为空")
+ private String bankName;
+ @NotBlank(message = "开户行名称不能为空")
+ private String branchName;
+ @NotBlank(message = "开户行联行号不能为空")
+ private String branchCode;
+
+ public String getCardNumber() { return cardNumber; }
+ public void setCardNumber(String value) { this.cardNumber = value; }
+ public String getProvinceCode() { return provinceCode; }
+ public void setProvinceCode(String value) { this.provinceCode = value; }
+ public String getCityCode() { return cityCode; }
+ public void setCityCode(String value) { this.cityCode = value; }
+ public String getBankName() { return bankName; }
+ public void setBankName(String value) { this.bankName = value; }
+ public String getBranchName() { return branchName; }
+ public void setBranchName(String value) { this.branchName = value; }
+ public String getBranchCode() { return branchCode; }
+ public void setBranchCode(String value) { this.branchCode = value; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardResponse.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardResponse.java
new file mode 100644
index 0000000..e53256c
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideBankCardResponse.java
@@ -0,0 +1,37 @@
+package com.tourguide.modules.finance.dto;
+
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import org.springframework.util.StringUtils;
+
+public class GuideBankCardResponse {
+ private final Integer id;
+ private final String bankName;
+ private final String branchName;
+ private final String cardMasked;
+ private final String status;
+ private final String reason;
+
+ public GuideBankCardResponse(GuideWithdrawalAccount account) {
+ this.id = account.getId();
+ this.bankName = account.getBankName();
+ this.branchName = account.getBankBranchName();
+ this.cardMasked = mask(account.getBankCardCode());
+ this.status = StringUtils.hasText(account.getBankCardStatus())
+ ? account.getBankCardStatus()
+ : StringUtils.hasText(account.getTokenNo()) ? "approved" : "local";
+ this.reason = account.getBankCardReason();
+ }
+
+ private String mask(String number) {
+ if (!StringUtils.hasText(number)) return "";
+ String value = number.trim();
+ return "****" + value.substring(Math.max(0, value.length() - 4));
+ }
+
+ public Integer getId() { return id; }
+ public String getBankName() { return bankName; }
+ public String getBranchName() { return branchName; }
+ public String getCardMasked() { return cardMasked; }
+ public String getStatus() { return status; }
+ public String getReason() { return reason; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementOnboardingRequest.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementOnboardingRequest.java
new file mode 100644
index 0000000..8bd4976
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementOnboardingRequest.java
@@ -0,0 +1,78 @@
+package com.tourguide.modules.finance.dto;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/** 导游结算进件资料;企业和个人使用同一入口,由 subjectType 区分。 */
+public class GuideSettlementOnboardingRequest {
+ @NotBlank(message = "进件类型不能为空")
+ @Pattern(regexp = "individual|enterprise", message = "进件类型不支持")
+ private String subjectType;
+ @Size(max = 128, message = "企业名称不能超过128个字符")
+ private String enterpriseName;
+ @Size(max = 18, message = "统一社会信用代码不能超过18个字符")
+ private String creditCode;
+ @NotBlank(message = "主体姓名不能为空")
+ @Size(max = 32, message = "主体姓名不能超过32个字符")
+ private String subjectName;
+ @NotBlank(message = "证件号码不能为空")
+ @Pattern(regexp = "^\\d{17}[\\dXx]$", message = "证件号码格式不正确")
+ private String subjectIdNumber;
+ @NotBlank(message = "证件有效期类型不能为空")
+ @Pattern(regexp = "fixed|long_term", message = "证件有效期类型不支持")
+ private String subjectCertificateValidityType;
+ @NotBlank(message = "证件有效期开始日期不能为空")
+ private String subjectCertificateBeginDate;
+ private String subjectCertificateEndDate;
+ private String licenseValidityType;
+ private String licenseBeginDate;
+ private String licenseEndDate;
+ private String registeredRegion;
+ private String registeredProvinceCode;
+ private String registeredCityCode;
+ private String registeredDistrictCode;
+ private String registeredAddress;
+ @NotBlank(message = "联系人姓名不能为空")
+ private String contactName;
+ @NotBlank(message = "联系人手机号不能为空")
+ @Pattern(regexp = "^1\\d{10}$", message = "联系人手机号格式不正确")
+ private String contactMobile;
+
+ public String getSubjectType() { return subjectType; }
+ public void setSubjectType(String value) { this.subjectType = value; }
+ public String getEnterpriseName() { return enterpriseName; }
+ public void setEnterpriseName(String value) { this.enterpriseName = value; }
+ public String getCreditCode() { return creditCode; }
+ public void setCreditCode(String value) { this.creditCode = value; }
+ public String getSubjectName() { return subjectName; }
+ public void setSubjectName(String value) { this.subjectName = value; }
+ public String getSubjectIdNumber() { return subjectIdNumber; }
+ public void setSubjectIdNumber(String value) { this.subjectIdNumber = value; }
+ public String getSubjectCertificateValidityType() { return subjectCertificateValidityType; }
+ public void setSubjectCertificateValidityType(String value) { this.subjectCertificateValidityType = value; }
+ public String getSubjectCertificateBeginDate() { return subjectCertificateBeginDate; }
+ public void setSubjectCertificateBeginDate(String value) { this.subjectCertificateBeginDate = value; }
+ public String getSubjectCertificateEndDate() { return subjectCertificateEndDate; }
+ public void setSubjectCertificateEndDate(String value) { this.subjectCertificateEndDate = value; }
+ public String getLicenseValidityType() { return licenseValidityType; }
+ public void setLicenseValidityType(String value) { this.licenseValidityType = value; }
+ public String getLicenseBeginDate() { return licenseBeginDate; }
+ public void setLicenseBeginDate(String value) { this.licenseBeginDate = value; }
+ public String getLicenseEndDate() { return licenseEndDate; }
+ public void setLicenseEndDate(String value) { this.licenseEndDate = value; }
+ public String getRegisteredRegion() { return registeredRegion; }
+ public void setRegisteredRegion(String value) { this.registeredRegion = value; }
+ public String getRegisteredProvinceCode() { return registeredProvinceCode; }
+ public void setRegisteredProvinceCode(String value) { this.registeredProvinceCode = value; }
+ public String getRegisteredCityCode() { return registeredCityCode; }
+ public void setRegisteredCityCode(String value) { this.registeredCityCode = value; }
+ public String getRegisteredDistrictCode() { return registeredDistrictCode; }
+ public void setRegisteredDistrictCode(String value) { this.registeredDistrictCode = value; }
+ public String getRegisteredAddress() { return registeredAddress; }
+ public void setRegisteredAddress(String value) { this.registeredAddress = value; }
+ public String getContactName() { return contactName; }
+ public void setContactName(String value) { this.contactName = value; }
+ public String getContactMobile() { return contactMobile; }
+ public void setContactMobile(String value) { this.contactMobile = value; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementStatusResponse.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementStatusResponse.java
new file mode 100644
index 0000000..8988822
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideSettlementStatusResponse.java
@@ -0,0 +1,23 @@
+package com.tourguide.modules.finance.dto;
+
+import java.time.LocalDateTime;
+
+public class GuideSettlementStatusResponse {
+ private final String status;
+ private final String subjectType;
+ private final String reason;
+ private final LocalDateTime submittedAt;
+
+ public GuideSettlementStatusResponse(String status, String subjectType, String reason,
+ LocalDateTime submittedAt) {
+ this.status = status;
+ this.subjectType = subjectType;
+ this.reason = reason;
+ this.submittedAt = submittedAt;
+ }
+
+ public String getStatus() { return status; }
+ public String getSubjectType() { return subjectType; }
+ public String getReason() { return reason; }
+ public LocalDateTime getSubmittedAt() { return submittedAt; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalCreateRequest.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalCreateRequest.java
new file mode 100644
index 0000000..d09f802
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalCreateRequest.java
@@ -0,0 +1,18 @@
+package com.tourguide.modules.finance.dto;
+
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+public class GuideWithdrawalCreateRequest {
+ @NotNull(message = "提现金额不能为空")
+ @DecimalMin(value = "0.01", message = "提现金额必须大于0")
+ private BigDecimal amount;
+ @NotNull(message = "提现银行卡不能为空")
+ private Integer bankCardId;
+
+ public BigDecimal getAmount() { return amount; }
+ public void setAmount(BigDecimal value) { this.amount = value; }
+ public Integer getBankCardId() { return bankCardId; }
+ public void setBankCardId(Integer value) { this.bankCardId = value; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalSummaryResponse.java b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalSummaryResponse.java
new file mode 100644
index 0000000..84c3ac0
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/dto/GuideWithdrawalSummaryResponse.java
@@ -0,0 +1,13 @@
+package com.tourguide.modules.finance.dto;
+
+import java.math.BigDecimal;
+
+public class GuideWithdrawalSummaryResponse {
+ private final BigDecimal availableAmount;
+
+ public GuideWithdrawalSummaryResponse(BigDecimal availableAmount) {
+ this.availableAmount = availableAmount;
+ }
+
+ public BigDecimal getAvailableAmount() { return availableAmount; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/entity/GuideBankBranch.java b/backend/src/main/java/com/tourguide/modules/finance/entity/GuideBankBranch.java
new file mode 100644
index 0000000..e59a0ed
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/entity/GuideBankBranch.java
@@ -0,0 +1,42 @@
+package com.tourguide.modules.finance.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.tourguide.common.persistence.LogicDeleteEntity;
+
+import java.time.LocalDateTime;
+
+@TableName("guide_bank_branch")
+public class GuideBankBranch extends LogicDeleteEntity {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String bankName;
+ private String branchName;
+ private String branchCode;
+ private String cityCode;
+ private Integer status;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updatedAt;
+
+ public Long getId() { return id; }
+ public void setId(Long value) { this.id = value; }
+ public String getBankName() { return bankName; }
+ public void setBankName(String value) { this.bankName = value; }
+ public String getBranchName() { return branchName; }
+ public void setBranchName(String value) { this.branchName = value; }
+ public String getBranchCode() { return branchCode; }
+ public void setBranchCode(String value) { this.branchCode = value; }
+ public String getCityCode() { return cityCode; }
+ public void setCityCode(String value) { this.cityCode = value; }
+ public Integer getStatus() { return status; }
+ public void setStatus(Integer value) { this.status = value; }
+ public LocalDateTime getCreatedAt() { return createdAt; }
+ public void setCreatedAt(LocalDateTime value) { this.createdAt = value; }
+ public LocalDateTime getUpdatedAt() { return updatedAt; }
+ public void setUpdatedAt(LocalDateTime value) { this.updatedAt = value; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/mapper/GuideBankBranchMapper.java b/backend/src/main/java/com/tourguide/modules/finance/mapper/GuideBankBranchMapper.java
new file mode 100644
index 0000000..26acf18
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/mapper/GuideBankBranchMapper.java
@@ -0,0 +1,7 @@
+package com.tourguide.modules.finance.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tourguide.modules.finance.entity.GuideBankBranch;
+
+public interface GuideBankBranchMapper extends BaseMapper<GuideBankBranch> {
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementScheduler.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementScheduler.java
new file mode 100644
index 0000000..bd166f8
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementScheduler.java
@@ -0,0 +1,39 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/** 周期扫描已到结算时间但尚未入账的订单。 */
+@Service
+public class GuideBalanceSettlementScheduler {
+ private static final Logger log = LoggerFactory.getLogger(GuideBalanceSettlementScheduler.class);
+ private static final int BATCH_SIZE = 100;
+
+ private final TourOrderMapper orderMapper;
+ private final GuideBalanceSettlementService settlementService;
+
+ public GuideBalanceSettlementScheduler(TourOrderMapper orderMapper,
+ GuideBalanceSettlementService settlementService) {
+ this.orderMapper = orderMapper;
+ this.settlementService = settlementService;
+ }
+
+ @Scheduled(fixedDelayString = "${tourguide.finance.settlement-scan-delay-millis:60000}")
+ public void settleDueOrders() {
+ LocalDateTime now = LocalDateTime.now();
+ List<Long> orderIds = orderMapper.selectDueSettlementIds(now, BATCH_SIZE);
+ for (Long orderId : orderIds) {
+ try {
+ settlementService.settle(orderId, now);
+ } catch (RuntimeException exception) {
+ log.error("Guide balance settlement failed, orderId={}", orderId, exception);
+ }
+ }
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementService.java
new file mode 100644
index 0000000..215d7bd
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideBalanceSettlementService.java
@@ -0,0 +1,53 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.modules.order.domain.OrderStatus;
+import com.tourguide.modules.order.domain.RefundStatus;
+import com.tourguide.modules.order.entity.TourOrder;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/** 将到期且未分账的订单收益幂等计入导游余额。 */
+@Service
+public class GuideBalanceSettlementService {
+ private final TourOrderMapper orderMapper;
+ private final GuideWithdrawableBalanceService balanceService;
+
+ public GuideBalanceSettlementService(TourOrderMapper orderMapper,
+ GuideWithdrawableBalanceService balanceService) {
+ this.orderMapper = orderMapper;
+ this.balanceService = balanceService;
+ }
+
+ @Transactional
+ public boolean settle(Long orderId, LocalDateTime settledAt) {
+ TourOrder order = orderMapper.selectForUpdate(orderId);
+ if (!canSettle(order, settledAt)) return false;
+
+ BigDecimal earning = order.getGuideEarningAmount();
+ if (earning.signum() > 0) {
+ balanceService.credit(order.getGuideId(), earning);
+ }
+ order.setSettledAt(settledAt);
+ if (orderMapper.updateById(order) != 1) {
+ throw new BusinessException(500, "订单分账状态更新失败");
+ }
+ return true;
+ }
+
+ private boolean canSettle(TourOrder order, LocalDateTime now) {
+ return order != null
+ && OrderStatus.COMPLETED.matches(order.getStatus())
+ && order.getSettledAt() == null
+ && order.getSettlementDueAt() != null
+ && !order.getSettlementDueAt().isAfter(now)
+ && order.getGuideId() != null
+ && order.getGuideEarningAmount() != null
+ && order.getGuideEarningAmount().signum() >= 0
+ && !RefundStatus.REFUNDED.matches(order.getRefundStatus());
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideSettlementService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideSettlementService.java
new file mode 100644
index 0000000..8443b2e
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideSettlementService.java
@@ -0,0 +1,513 @@
+package com.tourguide.modules.finance.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.common.huifu.biz.DouGongBasicDataBiz;
+import com.tourguide.modules.finance.dto.GuideBankBranchResponse;
+import com.tourguide.modules.finance.dto.GuideBankCardRequest;
+import com.tourguide.modules.finance.dto.GuideBankCardResponse;
+import com.tourguide.modules.finance.dto.GuideSettlementOnboardingRequest;
+import com.tourguide.modules.finance.dto.GuideSettlementStatusResponse;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import com.tourguide.modules.finance.entity.GuideBankBranch;
+import com.tourguide.modules.finance.mapper.GuideBankBranchMapper;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalAccountMapper;
+import com.tourguide.modules.user.entity.GuideUser;
+import com.tourguide.modules.user.service.GuideUserService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/** 导游端进件、审核状态和当前结算卡能力。 */
+@Service
+public class GuideSettlementService {
+ static final int GUIDE_USER_TYPE = 2;
+ static final int HUIFU_TYPE = 1;
+ static final int ACTIVE_STATUS = 1;
+ private static final int AUDIT_PENDING = 1;
+ private static final int AUDIT_APPROVED = 2;
+ private static final int AUDIT_REJECTED = 3;
+ private static final String SUCCESS_CODE = "00000000";
+
+ private final GuideWithdrawalAccountMapper accountMapper;
+ private final GuideUserService guideUserService;
+ private final DouGongBasicDataBiz douGongBasicDataBiz;
+ private final GuideBankBranchMapper bankBranchMapper;
+
+ public GuideSettlementService(GuideWithdrawalAccountMapper accountMapper,
+ GuideUserService guideUserService,
+ DouGongBasicDataBiz douGongBasicDataBiz,
+ GuideBankBranchMapper bankBranchMapper) {
+ this.accountMapper = accountMapper;
+ this.guideUserService = guideUserService;
+ this.douGongBasicDataBiz = douGongBasicDataBiz;
+ this.bankBranchMapper = bankBranchMapper;
+ }
+
+ public GuideSettlementStatusResponse status(Long guideId) {
+ guideUserService.findEnabledById(guideId);
+ GuideWithdrawalAccount account = findCurrent(guideId);
+ if (account == null) return new GuideSettlementStatusResponse("unsubmitted", null, null, null);
+ refreshAuditState(account);
+ return toStatus(account);
+ }
+
+ @Transactional(noRollbackFor = BusinessException.class)
+ public GuideSettlementStatusResponse submit(Long guideId, GuideSettlementOnboardingRequest request) {
+ GuideUser guide = guideUserService.findEnabledById(guideId);
+ validateOnboarding(request);
+ GuideWithdrawalAccount account = findCurrent(guideId);
+ if (account != null && "approved".equals(statusValue(account))) {
+ throw new BusinessException(409, "当前导游已完成进件");
+ }
+ if (account != null && "pending".equals(statusValue(account))) {
+ throw new BusinessException(409, "进件资料正在审核中,请勿重复提交");
+ }
+ if (account != null && StringUtils.hasText(account.getHuifuId())
+ && StringUtils.hasText(account.getSubjectType())
+ && !account.getSubjectType().equals(request.getSubjectType())) {
+ throw new BusinessException(409, "已提交的进件主体类型不可变更,请按原类型重新提交");
+ }
+ if (account == null) account = new GuideWithdrawalAccount();
+ applyOnboarding(account, guideId, request, guide);
+ account.setApplicationStatus("processing");
+ account.setIsAudit(AUDIT_PENDING);
+ account.setFailureMsgsReason(null);
+ account.setOutRequestNo(randomRequestNo());
+ account.setCreateTime(account.getCreateTime() == null ? LocalDateTime.now() : account.getCreateTime());
+ save(account);
+ try {
+ boolean enterprise = "enterprise".equals(account.getSubjectType());
+ Map<String, Object> result = enterprise
+ ? submitEnterprise(account)
+ : submitIndividual(account);
+ requireSuccess(result, "斗拱进件提交失败");
+ applyRemoteIds(account, result);
+ if (enterprise) {
+ applyRemoteAuditState(account, result, false);
+ } else {
+ applyIndividualSuccess(account);
+ }
+ save(account);
+ return toStatus(account);
+ } catch (BusinessException exception) {
+ markRejected(account, exception.getMessage());
+ throw exception;
+ } catch (Exception exception) {
+ markRejected(account, exception.getMessage());
+ throw new BusinessException(502, "斗拱进件服务暂不可用,请稍后重试");
+ }
+ }
+
+ public List<GuideBankCardResponse> bankCards(Long guideId) {
+ GuideWithdrawalAccount account = requireApproved(guideId);
+ if (!StringUtils.hasText(account.getBankCardCode())) return Collections.emptyList();
+ refreshBankCardState(account);
+ return Collections.singletonList(new GuideBankCardResponse(account));
+ }
+
+ @Transactional
+ public GuideBankCardResponse bindBankCard(Long guideId, GuideBankCardRequest request) {
+ guideUserService.findEnabledById(guideId);
+ GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId));
+ requireApproved(account);
+ GuideBankBranch branch = findActiveBranch(request.getBranchCode(), request.getCityCode());
+ account.setBankName(branch.getBankName());
+ account.setBankCardCode(trim(request.getCardNumber()));
+ account.setBankBranchName(branch.getBranchName());
+ account.setBankBranchCode(branch.getBranchCode());
+ account.setBankCardProvinceCode(trim(request.getProvinceCode()));
+ account.setBankCardCityCode(branch.getCityCode());
+ account.setOpenAccountName(account.getCertificateName());
+ account.setBankCardStatus("local");
+ account.setBankCardReason(null);
+ account.setTokenNo(null);
+ account.setCusApplicationId(null);
+ save(account);
+ return new GuideBankCardResponse(account);
+ }
+
+ @Transactional(noRollbackFor = BusinessException.class)
+ public GuideBankCardResponse openBankCardBusiness(Long guideId, Integer cardId) {
+ guideUserService.findEnabledById(guideId);
+ GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId));
+ requireApproved(account);
+ requireCurrentBankCard(account, cardId);
+ if (StringUtils.hasText(account.getTokenNo())) {
+ account.setBankCardStatus("approved");
+ account.setBankCardReason(null);
+ save(account);
+ return new GuideBankCardResponse(account);
+ }
+ if ("pending".equals(account.getBankCardStatus())
+ && StringUtils.hasText(account.getCusApplicationId())) {
+ refreshBankCardState(account);
+ return new GuideBankCardResponse(account);
+ }
+ account.setBankCardStatus("pending");
+ account.setBankCardReason(null);
+ account.setCusApplicationId(null);
+ save(account);
+ try {
+ Map<String, Object> result = douGongBasicDataBiz.userBusinessOpen(account);
+ requireSuccess(result, "银行卡业务入驻失败");
+ account.setTokenNo(first(result, "token_no", "card_token_no"));
+ account.setCusApplicationId(first(result, "apply_no", "application_no", "cus_application_id"));
+ if (!StringUtils.hasText(account.getTokenNo())
+ && !StringUtils.hasText(account.getCusApplicationId())) {
+ throw new BusinessException(502, "银行卡业务入驻响应缺少取现卡序列号");
+ }
+ account.setBankCardStatus(StringUtils.hasText(account.getTokenNo()) ? "approved" : "pending");
+ save(account);
+ return new GuideBankCardResponse(account);
+ } catch (BusinessException exception) {
+ account.setBankCardStatus("rejected");
+ account.setBankCardReason(exception.getMessage());
+ save(account);
+ throw exception;
+ } catch (Exception exception) {
+ account.setBankCardStatus("rejected");
+ account.setBankCardReason(exception.getMessage());
+ save(account);
+ throw new BusinessException(502, "银行卡业务入驻服务暂不可用,请稍后重试");
+ }
+ }
+
+ private void requireCurrentBankCard(GuideWithdrawalAccount account, Integer cardId) {
+ if (cardId == null || !cardId.equals(account.getId())
+ || !StringUtils.hasText(account.getBankCardCode())) {
+ throw new BusinessException(404, "银行卡不存在,请重新选择");
+ }
+ }
+
+ private GuideBankBranch findActiveBranch(String branchCode, String cityCode) {
+ GuideBankBranch branch = bankBranchMapper.selectOne(new LambdaQueryWrapper<GuideBankBranch>()
+ .eq(GuideBankBranch::getBranchCode, trim(branchCode))
+ .eq(GuideBankBranch::getCityCode, trim(cityCode))
+ .eq(GuideBankBranch::getStatus, ACTIVE_STATUS)
+ .last("LIMIT 1"));
+ if (branch == null) {
+ throw new BusinessException(400, "开户行不存在或已停用,请重新选择");
+ }
+ return branch;
+ }
+
+ public List<GuideBankBranchResponse> branches(String cityCode, String keyword) {
+ String query = trim(keyword);
+ LambdaQueryWrapper<GuideBankBranch> wrapper = new LambdaQueryWrapper<GuideBankBranch>()
+ .eq(GuideBankBranch::getStatus, 1)
+ .eq(StringUtils.hasText(cityCode), GuideBankBranch::getCityCode,
+ StringUtils.hasText(cityCode) ? cityCode.trim() : null)
+ .and(StringUtils.hasText(query), item -> item.like(GuideBankBranch::getBankName, query)
+ .or().like(GuideBankBranch::getBranchName, query))
+ .orderByAsc(GuideBankBranch::getBankName)
+ .orderByAsc(GuideBankBranch::getBranchName)
+ .last("LIMIT 100");
+ return bankBranchMapper.selectList(wrapper).stream()
+ .map(GuideBankBranchResponse::new)
+ .collect(Collectors.toList());
+ }
+
+ public GuideWithdrawalAccount requireApproved(Long guideId) {
+ guideUserService.findEnabledById(guideId);
+ return requireApproved(findCurrent(guideId));
+ }
+
+ public GuideWithdrawalAccount requireApprovedCard(Long guideId, Integer cardId) {
+ GuideWithdrawalAccount account = requireApproved(guideId);
+ return requireApprovedCard(account, cardId);
+ }
+
+ /** 提现创建事务内锁定当前进件账户,串行校验余额和结算卡。 */
+ public GuideWithdrawalAccount requireApprovedCardForUpdate(Long guideId, Integer cardId) {
+ guideUserService.findEnabledById(guideId);
+ GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId));
+ requireApproved(account);
+ return requireApprovedCard(account, cardId);
+ }
+
+ private GuideWithdrawalAccount requireApprovedCard(GuideWithdrawalAccount account, Integer cardId) {
+ if (!account.getId().equals(cardId) || !StringUtils.hasText(account.getTokenNo())
+ || "rejected".equals(account.getBankCardStatus())) {
+ throw new BusinessException(409, "请选择审核通过的银行卡");
+ }
+ return account;
+ }
+
+ public GuideWithdrawalAccount findCurrent(Long guideId) {
+ return accountMapper.selectOne(new LambdaQueryWrapper<GuideWithdrawalAccount>()
+ .eq(GuideWithdrawalAccount::getUserId, toTableUserId(guideId))
+ .eq(GuideWithdrawalAccount::getUserType, GUIDE_USER_TYPE)
+ .eq(GuideWithdrawalAccount::getType, HUIFU_TYPE)
+ .eq(GuideWithdrawalAccount::getStatus, ACTIVE_STATUS)
+ .orderByDesc(GuideWithdrawalAccount::getId)
+ .last("LIMIT 1"));
+ }
+
+ private Map<String, Object> submitEnterprise(GuideWithdrawalAccount account) throws Exception {
+ return StringUtils.hasText(account.getHuifuId())
+ ? douGongBasicDataBiz.userBasicDataEntModify(account)
+ : douGongBasicDataBiz.userBasicDataEnt(account);
+ }
+
+ private Map<String, Object> submitIndividual(GuideWithdrawalAccount account) throws Exception {
+ return StringUtils.hasText(account.getHuifuId())
+ ? douGongBasicDataBiz.userBasicDataIndvModify(account)
+ : douGongBasicDataBiz.userBasicDataIndv(account);
+ }
+
+ private void refreshAuditState(GuideWithdrawalAccount account) {
+ if (!"pending".equals(statusValue(account)) || !StringUtils.hasText(account.getHuifuId())) return;
+ // 个人基础进件是同步接口:成功响应只有 huifu_id,不会返回申请单号,也无需查询审核状态。
+ if (!"enterprise".equals(account.getSubjectType())) {
+ applyIndividualSuccess(account);
+ save(account);
+ return;
+ }
+ if (!StringUtils.hasText(account.getCusApplicationId())) return;
+ try {
+ Map<String, Object> result = douGongBasicDataBiz.userApplyQuery(account, account.getCusApplicationId());
+ requireSuccess(result, "进件审核状态查询失败");
+ applyRemoteAuditState(account, result, true);
+ save(account);
+ } catch (BusinessException exception) {
+ throw exception;
+ } catch (Exception exception) {
+ throw new BusinessException(502, "进件审核状态查询失败,请稍后重试");
+ }
+ }
+
+ private void refreshBankCardState(GuideWithdrawalAccount account) {
+ if (!"pending".equals(account.getBankCardStatus())
+ || !StringUtils.hasText(account.getHuifuId())
+ || !StringUtils.hasText(account.getCusApplicationId())) return;
+ try {
+ Map<String, Object> result = douGongBasicDataBiz.userApplyQuery(account, account.getCusApplicationId());
+ requireSuccess(result, "银行卡审核状态查询失败");
+ String remoteStatus = remoteStatus(result);
+ String tokenNo = first(result, "token_no", "card_token_no");
+ if (StringUtils.hasText(tokenNo)) account.setTokenNo(tokenNo);
+ if (isApproved(remoteStatus) || StringUtils.hasText(account.getTokenNo())) {
+ account.setBankCardStatus("approved");
+ account.setBankCardReason(null);
+ } else if (isRejected(remoteStatus)) {
+ account.setBankCardStatus("rejected");
+ account.setBankCardReason(remoteReason(result));
+ }
+ save(account);
+ } catch (Exception ignored) {
+ // 备注:银行卡列表仍返回本地最后状态,避免第三方短暂不可用导致页面完全不可用。
+ }
+ }
+
+ private void applyRemoteAuditState(GuideWithdrawalAccount account, Map<String, Object> result,
+ boolean keepPendingWhenUnknown) {
+ String remoteStatus = remoteStatus(result);
+ if (isApproved(remoteStatus)) {
+ account.setIsAudit(AUDIT_APPROVED);
+ account.setApplicationStatus("succeeded");
+ account.setSucceededAt(account.getSucceededAt() == null ? LocalDateTime.now() : account.getSucceededAt());
+ account.setFailureMsgsReason(null);
+ } else if (isRejected(remoteStatus)) {
+ account.setIsAudit(AUDIT_REJECTED);
+ account.setApplicationStatus("failed");
+ account.setFailureMsgsReason(remoteReason(result));
+ } else if (keepPendingWhenUnknown || !StringUtils.hasText(remoteStatus)) {
+ account.setIsAudit(AUDIT_PENDING);
+ account.setApplicationStatus("submitted");
+ } else {
+ account.setIsAudit(AUDIT_PENDING);
+ account.setApplicationStatus(remoteStatus);
+ }
+ }
+
+ private void applyIndividualSuccess(GuideWithdrawalAccount account) {
+ if (!StringUtils.hasText(account.getHuifuId())) {
+ throw new BusinessException(502, "斗拱个人进件成功响应缺少汇付ID");
+ }
+ account.setIsAudit(AUDIT_APPROVED);
+ account.setApplicationStatus("succeeded");
+ account.setSucceededAt(account.getSucceededAt() == null ? LocalDateTime.now() : account.getSucceededAt());
+ account.setFailureMsgsReason(null);
+ account.setCusApplicationId(null);
+ }
+
+ private GuideWithdrawalAccount requireApproved(GuideWithdrawalAccount account) {
+ if (account == null) throw new BusinessException(409, "请先完成导游进件");
+ if (!"approved".equals(statusValue(account))) {
+ throw new BusinessException(409, "进件审核通过后才能继续操作");
+ }
+ if (!StringUtils.hasText(account.getHuifuId())) {
+ throw new BusinessException(409, "进件账户信息不完整,请联系平台处理");
+ }
+ return account;
+ }
+
+ private GuideSettlementStatusResponse toStatus(GuideWithdrawalAccount account) {
+ return new GuideSettlementStatusResponse(statusValue(account),
+ StringUtils.hasText(account.getSubjectType()) ? account.getSubjectType() : "individual",
+ account.getFailureMsgsReason(), account.getCreateTime());
+ }
+
+ private String statusValue(GuideWithdrawalAccount account) {
+ if (account == null) return "unsubmitted";
+ if (Integer.valueOf(AUDIT_APPROVED).equals(account.getIsAudit())) return "approved";
+ if (Integer.valueOf(AUDIT_REJECTED).equals(account.getIsAudit())) return "rejected";
+ String value = account.getApplicationStatus();
+ if (isApproved(value) && StringUtils.hasText(account.getHuifuId())) return "approved";
+ if (isRejected(value)) return "rejected";
+ return "pending";
+ }
+
+ private void validateOnboarding(GuideSettlementOnboardingRequest request) {
+ boolean enterprise = "enterprise".equals(request.getSubjectType());
+ if (enterprise) {
+ requireText(request.getEnterpriseName(), "企业名称不能为空");
+ if (!StringUtils.hasText(request.getCreditCode())
+ || !request.getCreditCode().trim().matches("^[0-9A-Z]{18}$")) {
+ throw new BusinessException(400, "统一社会信用代码格式不正确");
+ }
+ requireValidity(request.getLicenseValidityType(), request.getLicenseBeginDate(),
+ request.getLicenseEndDate(), "营业执照");
+ requireText(request.getRegisteredProvinceCode(), "注册地址省编码不能为空");
+ requireText(request.getRegisteredCityCode(), "注册地址市编码不能为空");
+ requireText(request.getRegisteredDistrictCode(), "注册地址区县编码不能为空");
+ requireText(request.getRegisteredAddress(), "详细注册地址不能为空");
+ }
+ requireValidity(request.getSubjectCertificateValidityType(),
+ request.getSubjectCertificateBeginDate(), request.getSubjectCertificateEndDate(), "证件");
+ }
+
+ private void requireValidity(String type, String begin, String end, String label) {
+ if (!"fixed".equals(type) && !"long_term".equals(type)) {
+ throw new BusinessException(400, label + "有效期类型不支持");
+ }
+ requireText(begin, label + "有效期开始日期不能为空");
+ if ("fixed".equals(type)) requireText(end, label + "有效期结束日期不能为空");
+ }
+
+ private void requireText(String value, String message) {
+ if (!StringUtils.hasText(value)) throw new BusinessException(400, message);
+ }
+
+ private void applyOnboarding(GuideWithdrawalAccount account, Long guideId,
+ GuideSettlementOnboardingRequest request, GuideUser guide) {
+ account.setUserId(toTableUserId(guideId));
+ account.setUserType(GUIDE_USER_TYPE);
+ account.setType(HUIFU_TYPE);
+ account.setStatus(ACTIVE_STATUS);
+ account.setSubjectType(request.getSubjectType());
+ account.setEnterpriseName(trim(request.getEnterpriseName()));
+ account.setCreditCode(trim(request.getCreditCode()));
+ account.setLicenseValidityType(trim(request.getLicenseValidityType()));
+ account.setLicenseBeginDate(trim(request.getLicenseBeginDate()));
+ account.setLicenseEndDate(trim(request.getLicenseEndDate()));
+ account.setRegisteredProvinceCode(trim(request.getRegisteredProvinceCode()));
+ account.setRegisteredCityCode(trim(request.getRegisteredCityCode()));
+ account.setRegisteredDistrictCode(trim(request.getRegisteredDistrictCode()));
+ account.setRegisteredAddress(trim(request.getRegisteredAddress()));
+ account.setContactName(trim(request.getContactName()));
+ account.setCertificateName(trim(request.getSubjectName()));
+ account.setCertificateType(0);
+ account.setCertificateCode(trim(request.getSubjectIdNumber()).toUpperCase());
+ account.setCertificateExpireTime(dateRange(request.getSubjectCertificateValidityType(),
+ request.getSubjectCertificateBeginDate(), request.getSubjectCertificateEndDate()));
+ account.setOpenAccountName(trim(request.getSubjectName()));
+ account.setPhone(StringUtils.hasText(request.getContactMobile())
+ ? request.getContactMobile().trim() : guide.getMobile());
+ }
+
+ private String dateRange(String type, String begin, String end) {
+ return trim(begin) + "," + ("long_term".equals(type) ? "长期" : trim(end));
+ }
+
+ private void applyRemoteIds(GuideWithdrawalAccount account, Map<String, Object> result) {
+ String huifuId = first(result, "huifu_id", "user_huifu_id");
+ String applicationId = first(result, "apply_no", "application_no", "cus_application_id");
+ if (StringUtils.hasText(huifuId)) account.setHuifuId(huifuId);
+ if (StringUtils.hasText(applicationId)) account.setCusApplicationId(applicationId);
+ }
+
+ private void requireSuccess(Map<String, Object> result, String fallback) {
+ String code = first(result, "resp_code", "code");
+ if (SUCCESS_CODE.equals(code)) return;
+ String message = first(result, "resp_desc", "message", "msg");
+ throw new BusinessException(502, StringUtils.hasText(message) ? message : fallback);
+ }
+
+ private String remoteStatus(Map<String, Object> result) {
+ return first(result, "apply_status", "application_status", "audit_status", "status", "trans_stat");
+ }
+
+ private String remoteReason(Map<String, Object> result) {
+ String reason = first(result, "failure_reason", "fail_reason", "audit_remark", "resp_desc");
+ return StringUtils.hasText(reason) ? reason : "进件审核未通过";
+ }
+
+ private boolean isApproved(String value) {
+ if (!StringUtils.hasText(value)) return false;
+ String normalized = value.trim().toLowerCase();
+ return Arrays.asList("2", "s", "success", "succeeded", "approved", "pass", "passed").contains(normalized);
+ }
+
+ private boolean isRejected(String value) {
+ if (!StringUtils.hasText(value)) return false;
+ String normalized = value.trim().toLowerCase();
+ return Arrays.asList("3", "f", "failed", "failure", "rejected", "reject").contains(normalized);
+ }
+
+ @SuppressWarnings("unchecked")
+ private String first(Map<String, Object> result, String... keys) {
+ if (result == null) return null;
+ for (String key : keys) {
+ Object value = result.get(key);
+ if (value != null && StringUtils.hasText(String.valueOf(value))) return String.valueOf(value);
+ }
+ Object data = result.get("data");
+ if (data instanceof Map) return first((Map<String, Object>) data, keys);
+ if (data instanceof String && StringUtils.hasText((String) data)) {
+ try {
+ Map<String, Object> nested = com.alibaba.fastjson.JSONObject.parseObject(
+ (String) data, LinkedHashMap.class);
+ return first(nested, keys);
+ } catch (Exception ignored) {}
+ }
+ return null;
+ }
+
+ private void markRejected(GuideWithdrawalAccount account, String reason) {
+ account.setIsAudit(AUDIT_REJECTED);
+ account.setApplicationStatus("failed");
+ account.setFailureMsgsReason(StringUtils.hasText(reason) ? reason : "斗拱进件失败");
+ save(account);
+ }
+
+ private void save(GuideWithdrawalAccount account) {
+ if (account.getId() == null) accountMapper.insert(account); else accountMapper.updateById(account);
+ }
+
+ private int toTableUserId(Long guideId) {
+ try {
+ return Math.toIntExact(guideId);
+ } catch (ArithmeticException exception) {
+ throw new BusinessException(400, "导游ID超出进件表支持范围");
+ }
+ }
+
+ private String randomRequestNo() {
+ return UUID.randomUUID().toString().replace("-", "");
+ }
+
+ private String trim(String value) {
+ return StringUtils.hasText(value) ? value.trim() : null;
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceService.java
new file mode 100644
index 0000000..fb10bf9
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceService.java
@@ -0,0 +1,39 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.modules.user.mapper.GuideUserMapper;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/** 统一读取和原子变更导游可提现余额。 */
+@Service
+public class GuideWithdrawableBalanceService {
+ private final GuideUserMapper guideUserMapper;
+
+ public GuideWithdrawableBalanceService(GuideUserMapper guideUserMapper) {
+ this.guideUserMapper = guideUserMapper;
+ }
+
+ public BigDecimal getAvailableAmount(Long guideId) {
+ return money(guideUserMapper.selectBalance(guideId));
+ }
+
+ public boolean debit(Long guideId, BigDecimal amount) {
+ BigDecimal value = money(amount);
+ return value.signum() > 0 && guideUserMapper.decreaseBalance(guideId, value) == 1;
+ }
+
+ public void credit(Long guideId, BigDecimal amount) {
+ BigDecimal value = money(amount);
+ if (value.signum() == 0) return;
+ if (value.signum() < 0 || guideUserMapper.increaseBalance(guideId, value) != 1) {
+ throw new BusinessException(500, "导游余额更新失败");
+ }
+ }
+
+ private BigDecimal money(BigDecimal value) {
+ return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP);
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutScheduler.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutScheduler.java
new file mode 100644
index 0000000..adfac08
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutScheduler.java
@@ -0,0 +1,39 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/** 恢复因网络异常或异步处理中断的划账、取现流程。 */
+@Service
+public class GuideWithdrawalPayoutScheduler {
+ private static final Logger log = LoggerFactory.getLogger(GuideWithdrawalPayoutScheduler.class);
+ private static final int BATCH_SIZE = 100;
+
+ private final GuideWithdrawalMapper withdrawalMapper;
+ private final GuideWithdrawalPayoutService payoutService;
+
+ public GuideWithdrawalPayoutScheduler(GuideWithdrawalMapper withdrawalMapper,
+ GuideWithdrawalPayoutService payoutService) {
+ this.withdrawalMapper = withdrawalMapper;
+ this.payoutService = payoutService;
+ }
+
+ @Scheduled(fixedDelayString = "${tourguide.finance.withdrawal-payout-scan-delay-millis:30000}")
+ public void resumePendingPayouts() {
+ List<Long> ids = withdrawalMapper.selectPendingPayoutIds(
+ LocalDateTime.now().minusSeconds(10), BATCH_SIZE);
+ for (Long id : ids) {
+ try {
+ payoutService.advance(id);
+ } catch (RuntimeException exception) {
+ log.error("恢复提现资金流程失败,withdrawalId={}", id, exception);
+ }
+ }
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutService.java
new file mode 100644
index 0000000..10ed1df
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutService.java
@@ -0,0 +1,182 @@
+package com.tourguide.modules.finance.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.tourguide.common.huifu.biz.DouGongWithdrawBiz;
+import com.tourguide.common.huifu.biz.FenZhangUtil;
+import com.tourguide.modules.finance.dto.WithdrawalResponse;
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/** 编排主账户划账、导游账户取现及异常后的原流水查询。 */
+@Service
+public class GuideWithdrawalPayoutService {
+ private static final Logger log = LoggerFactory.getLogger(GuideWithdrawalPayoutService.class);
+ private static final String UNKNOWN_ERROR_CODE = "9999";
+
+ private final GuideWithdrawalStateService stateService;
+ private final GuideSettlementService settlementService;
+ private final FenZhangUtil fenZhangUtil;
+ private final DouGongWithdrawBiz withdrawBiz;
+
+ public GuideWithdrawalPayoutService(GuideWithdrawalStateService stateService,
+ GuideSettlementService settlementService,
+ FenZhangUtil fenZhangUtil,
+ DouGongWithdrawBiz withdrawBiz) {
+ this.stateService = stateService;
+ this.settlementService = settlementService;
+ this.fenZhangUtil = fenZhangUtil;
+ this.withdrawBiz = withdrawBiz;
+ }
+
+ public WithdrawalResponse approve(Long id, String remark, String processor) {
+ GuideWithdrawal withdrawal = stateService.prepareTransfer(id, remark, processor);
+ submitTransfer(withdrawal);
+ return new WithdrawalResponse(stateService.find(id));
+ }
+
+ public void advance(Long id) {
+ GuideWithdrawal withdrawal = stateService.find(id);
+ if (GuideWithdrawalStateService.TRANSFERRING.equals(withdrawal.getStatus())) {
+ resolveTransfer(withdrawal);
+ } else if (GuideWithdrawalStateService.TRANSFERRED.equals(withdrawal.getStatus())) {
+ if (StringUtils.hasText(withdrawal.getCashReqSeqId())
+ && StringUtils.hasText(withdrawal.getCashReqDate())) {
+ GuideWithdrawal processing = stateService.resumeCashQuery(withdrawal.getId());
+ if (processing != null) resolveCash(processing);
+ } else {
+ submitCash(withdrawal.getId());
+ }
+ } else if (GuideWithdrawalStateService.PROCESSING.equals(withdrawal.getStatus())) {
+ resolveCash(withdrawal);
+ }
+ }
+
+ private void submitTransfer(GuideWithdrawal withdrawal) {
+ try {
+ GuideWithdrawalAccount account = settlementService.requireApprovedCard(
+ withdrawal.getGuideId(), withdrawal.getBankCardId());
+ Map<String, Object> response = fenZhangUtil.fenzhang(
+ withdrawal.getTransferReqSeqId(), withdrawal.getTransferReqDate(),
+ withdrawal.getAmount().toString(), account.getHuifuId());
+ handleTransferResponse(withdrawal, response);
+ } catch (Exception exception) {
+ log.error("主账户划账调用异常,withdrawalId={},reqSeqId={}",
+ withdrawal.getId(), withdrawal.getTransferReqSeqId(), exception);
+ stateService.markTransferPending(withdrawal.getId(), null,
+ "主账户划账结果待查询:" + exception.getMessage());
+ }
+ }
+
+ private void handleTransferResponse(GuideWithdrawal withdrawal, Map<String, Object> response) {
+ if (transactionSucceeded(response)) {
+ stateService.markTransferred(withdrawal.getId(), value(response, "hf_seq_id"));
+ submitCash(withdrawal.getId());
+ return;
+ }
+ if (transactionFailed(response) || businessRejected(response)) {
+ stateService.markTransferFailed(withdrawal.getId(),
+ "主账户划账失败:" + message(response));
+ return;
+ }
+ stateService.markTransferPending(withdrawal.getId(), value(response, "hf_seq_id"),
+ "主账户划账处理中:" + message(response));
+ }
+
+ private void resolveTransfer(GuideWithdrawal withdrawal) {
+ try {
+ Map<String, Object> response = fenZhangUtil.queryFenzhang(
+ withdrawal.getTransferReqSeqId(), withdrawal.getTransferReqDate());
+ if (transactionSucceeded(response)) {
+ stateService.markTransferred(withdrawal.getId(), value(response, "hf_seq_id"));
+ submitCash(withdrawal.getId());
+ } else if (transactionFailed(response)) {
+ stateService.markTransferFailed(withdrawal.getId(),
+ "主账户划账失败:" + message(response));
+ } else {
+ stateService.markTransferPending(withdrawal.getId(), value(response, "hf_seq_id"),
+ (businessRejected(response) ? "主账户划账查询失败:" : "主账户划账结果待确认:")
+ + message(response));
+ }
+ } catch (Exception exception) {
+ log.warn("查询主账户划账状态失败,withdrawalId={},原因={}",
+ withdrawal.getId(), exception.getMessage());
+ stateService.markTransferPending(withdrawal.getId(), null,
+ "主账户划账结果待查询:" + exception.getMessage());
+ }
+ }
+
+ private void submitCash(Long withdrawalId) {
+ GuideWithdrawal withdrawal = stateService.prepareCash(withdrawalId);
+ if (withdrawal == null) return;
+ GuideWithdrawalAccount account = settlementService.requireApprovedCard(
+ withdrawal.getGuideId(), withdrawal.getBankCardId());
+ JSONObject response = withdrawBiz.encashment(withdrawal.getAmount(), account.getHuifuId(),
+ account.getTokenNo(), withdrawal.getCashReqSeqId(), withdrawal.getCashReqDate());
+ if (businessRejected(response) || transactionFailed(response)) {
+ stateService.markCashRetry(withdrawal.getId(), "导游账户取现失败:" + message(response));
+ return;
+ }
+ stateService.markCashPending(withdrawal.getId(), value(response, "hf_seq_id"),
+ "导游账户取现已受理:" + message(response));
+ }
+
+ private void resolveCash(GuideWithdrawal withdrawal) {
+ GuideWithdrawalAccount account = settlementService.requireApprovedCard(
+ withdrawal.getGuideId(), withdrawal.getBankCardId());
+ JSONObject response = withdrawBiz.query(account.getHuifuId(),
+ withdrawal.getCashReqDate(), withdrawal.getCashReqSeqId());
+ if (transactionSucceeded(response)) {
+ stateService.markApproved(withdrawal.getId(), value(response, "hf_seq_id"));
+ } else if (transactionFailed(response)) {
+ stateService.markCashRetry(withdrawal.getId(), "导游账户取现失败:" + message(response));
+ } else {
+ stateService.markCashPending(withdrawal.getId(), value(response, "hf_seq_id"),
+ (businessRejected(response) ? "导游账户取现查询失败:" : "导游账户取现结果待确认:")
+ + message(response));
+ }
+ }
+
+ private boolean transactionSucceeded(Map<String, ?> response) {
+ String status = value(response, "trans_stat", "cash_stat", "status");
+ return "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)
+ || "SUCCEEDED".equalsIgnoreCase(status);
+ }
+
+ private boolean transactionFailed(Map<String, ?> response) {
+ String status = value(response, "trans_stat", "cash_stat", "status");
+ return "F".equalsIgnoreCase(status) || "FAIL".equalsIgnoreCase(status)
+ || "FAILED".equalsIgnoreCase(status);
+ }
+
+ private boolean businessRejected(Map<String, ?> response) {
+ String code = value(response, "resp_code", "code");
+ return StringUtils.hasText(code)
+ && !UNKNOWN_ERROR_CODE.equals(code)
+ && !Arrays.asList("00000000", "00000100").contains(code);
+ }
+
+ private String message(Map<String, ?> response) {
+ if (response == null) return "斗拱服务无响应";
+ String message = value(response, "resp_desc", "msg", "message");
+ return StringUtils.hasText(message) ? message : "等待斗拱返回最终状态";
+ }
+
+ @SuppressWarnings("unchecked")
+ private String value(Map<String, ?> response, String... keys) {
+ if (response == null) return null;
+ for (String key : keys) {
+ Object value = response.get(key);
+ if (value != null && StringUtils.hasText(value.toString())) return value.toString();
+ }
+ Object data = response.get("data");
+ if (data instanceof Map) return value((Map<String, ?>) data, keys);
+ return null;
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalService.java
new file mode 100644
index 0000000..fc3c02a
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalService.java
@@ -0,0 +1,236 @@
+package com.tourguide.modules.finance.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tourguide.common.api.PageResponse;
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.modules.finance.dto.GuideWithdrawalCreateRequest;
+import com.tourguide.modules.finance.dto.GuideWithdrawalSummaryResponse;
+import com.tourguide.modules.finance.dto.WithdrawalResponse;
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper;
+import com.tourguide.modules.user.entity.GuideUser;
+import com.tourguide.modules.user.service.GuideUserService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+/** 导游提现申请、审核发起和斗拱到账回调。 */
+@Service
+public class GuideWithdrawalService {
+ private static final DateTimeFormatter NO_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
+ private static final String STATUS_PENDING = "PENDING";
+ private static final String STATUS_APPROVED = "APPROVED";
+ private static final String STATUS_REJECTED = "REJECTED";
+
+ private final GuideWithdrawalMapper withdrawalMapper;
+ private final GuideSettlementService settlementService;
+ private final GuideUserService guideUserService;
+ private final GuideWithdrawableBalanceService balanceService;
+ private final GuideWithdrawalStateService stateService;
+ private final GuideWithdrawalPayoutService payoutService;
+
+ public GuideWithdrawalService(GuideWithdrawalMapper withdrawalMapper,
+ GuideSettlementService settlementService,
+ GuideUserService guideUserService,
+ GuideWithdrawableBalanceService balanceService,
+ GuideWithdrawalStateService stateService,
+ GuideWithdrawalPayoutService payoutService) {
+ this.withdrawalMapper = withdrawalMapper;
+ this.settlementService = settlementService;
+ this.guideUserService = guideUserService;
+ this.balanceService = balanceService;
+ this.stateService = stateService;
+ this.payoutService = payoutService;
+ }
+
+ public GuideWithdrawalSummaryResponse summary(Long guideId) {
+ try {
+ settlementService.requireApproved(guideId);
+ return new GuideWithdrawalSummaryResponse(balanceService.getAvailableAmount(guideId));
+ } catch (BusinessException exception) {
+ if (exception.getCode() == 409) {
+ return new GuideWithdrawalSummaryResponse(money(BigDecimal.ZERO));
+ }
+ throw exception;
+ }
+ }
+
+ public PageResponse<WithdrawalResponse> page(Long guideId, long page, long size) {
+ guideUserService.findEnabledById(guideId);
+ long safePage = Math.max(page, 1);
+ long safeSize = Math.min(Math.max(size, 1), 100);
+ Page<GuideWithdrawal> result = withdrawalMapper.selectPage(
+ new Page<GuideWithdrawal>(safePage, safeSize),
+ new LambdaQueryWrapper<GuideWithdrawal>()
+ .eq(GuideWithdrawal::getGuideId, guideId)
+ .orderByDesc(GuideWithdrawal::getAppliedAt)
+ .orderByDesc(GuideWithdrawal::getId));
+ return new PageResponse<WithdrawalResponse>(result.getRecords().stream()
+ .map(WithdrawalResponse::new).collect(Collectors.toList()),
+ result.getTotal(), result.getCurrent(), result.getSize());
+ }
+
+ @Transactional
+ public WithdrawalResponse create(Long guideId, GuideWithdrawalCreateRequest request) {
+ GuideUser guide = guideUserService.findEnabledById(guideId);
+ GuideWithdrawalAccount account = settlementService.requireApprovedCardForUpdate(
+ guideId, request.getBankCardId());
+ BigDecimal amount = money(request.getAmount());
+ if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new BusinessException(400, "提现金额必须大于0");
+ }
+ if (!balanceService.debit(guideId, amount)) {
+ throw new BusinessException(409, "提现金额不能超过当前可提金额");
+ }
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setWithdrawalNo(generateWithdrawalNo());
+ withdrawal.setGuideId(guideId);
+ withdrawal.setGuideName(StringUtils.hasText(guide.getRealName())
+ ? guide.getRealName() : guide.getUsername());
+ withdrawal.setGuideMobile(guide.getMobile());
+ withdrawal.setAmount(amount);
+ withdrawal.setBankCardId(account.getId());
+ withdrawal.setBankName(account.getBankName());
+ withdrawal.setCardMasked(maskCard(account.getBankCardCode()));
+ withdrawal.setStatus(STATUS_PENDING);
+ withdrawal.setAppliedAt(LocalDateTime.now());
+ withdrawal.setCreatedAt(LocalDateTime.now());
+ withdrawal.setUpdatedAt(LocalDateTime.now());
+ withdrawalMapper.insert(withdrawal);
+ return new WithdrawalResponse(withdrawal);
+ }
+
+ public WithdrawalResponse process(Long id, String status, String remark, String processor) {
+ if (STATUS_REJECTED.equals(status)) {
+ return stateService.reject(id, remark, processor);
+ }
+ if (!STATUS_APPROVED.equals(status)) throw new BusinessException(400, "提现处理状态不正确");
+ return payoutService.approve(id, remark, processor);
+ }
+
+ @Transactional
+ public boolean handleCallback(JSONObject body) {
+ String requestSequence = first(body, "req_seq_id", "org_req_seq_id");
+ if (!StringUtils.hasText(requestSequence)) {
+ throw new BusinessException(400, "汇付提现回调缺少请求流水号");
+ }
+ GuideWithdrawal withdrawal = withdrawalMapper.selectByPayoutSequenceForUpdate(requestSequence);
+ if (withdrawal == null) throw new BusinessException(404, "提现申请不存在");
+ if (STATUS_APPROVED.equals(withdrawal.getStatus()) || STATUS_REJECTED.equals(withdrawal.getStatus())) {
+ return false;
+ }
+ boolean processing = GuideWithdrawalStateService.PROCESSING.equals(withdrawal.getStatus());
+ boolean recoverableTransferred = GuideWithdrawalStateService.TRANSFERRED.equals(withdrawal.getStatus())
+ && requestSequence.equals(withdrawal.getCashReqSeqId());
+ if (!processing && !recoverableTransferred) {
+ return false;
+ }
+ validateCallback(body, withdrawal);
+ String transStatus = first(body, "trans_stat", "cash_stat", "status");
+ LocalDateTime now = LocalDateTime.now();
+ if (isFailure(body, transStatus)) {
+ String message = "导游账户取现失败:" + callbackMessage(body);
+ if (withdrawalMapper.resetCashForRetry(withdrawal.getId(), message, now, now) != 1) {
+ throw new BusinessException(500, "提现回调状态更新失败");
+ }
+ withdrawal.setStatus(GuideWithdrawalStateService.TRANSFERRED);
+ withdrawal.setCashReqSeqId(null);
+ withdrawal.setCashReqDate(null);
+ withdrawal.setCashHfSeqId(null);
+ withdrawal.setPayoutMessage(message);
+ withdrawal.setProcessedAt(now);
+ withdrawal.setUpdatedAt(now);
+ return true;
+ }
+ String hfSeqId = first(body, "hf_seq_id", "trans_seq_id");
+ if (StringUtils.hasText(hfSeqId)) withdrawal.setCashHfSeqId(hfSeqId);
+ if (isSuccess(transStatus)) {
+ withdrawal.setStatus(STATUS_APPROVED);
+ withdrawal.setPayoutMessage("取现到账成功");
+ withdrawal.setProcessedAt(now);
+ } else {
+ withdrawal.setStatus(GuideWithdrawalStateService.PROCESSING);
+ withdrawal.setPayoutMessage("导游账户取现结果待确认:" + callbackMessage(body));
+ }
+ withdrawal.setUpdatedAt(now);
+ if (withdrawalMapper.updateById(withdrawal) != 1) {
+ throw new BusinessException(500, "提现回调状态更新失败");
+ }
+ return true;
+ }
+
+ private void validateCallback(JSONObject body, GuideWithdrawal withdrawal) {
+ GuideWithdrawalAccount account = settlementService.findCurrent(withdrawal.getGuideId());
+ String callbackHuifuId = first(body, "huifu_id", "out_huifu_id");
+ if (account == null || !StringUtils.hasText(callbackHuifuId)
+ || !callbackHuifuId.equals(account.getHuifuId())) {
+ throw new BusinessException(400, "汇付提现回调账户不匹配");
+ }
+ String amountText = first(body, "cash_amt", "trans_amt", "ord_amt");
+ if (StringUtils.hasText(amountText)) {
+ try {
+ if (withdrawal.getAmount().compareTo(new BigDecimal(amountText)) != 0) {
+ throw new BusinessException(409, "汇付提现回调金额不匹配");
+ }
+ } catch (NumberFormatException exception) {
+ throw new BusinessException(400, "汇付提现回调金额格式错误");
+ }
+ }
+ }
+
+ private String callbackMessage(JSONObject body) {
+ String message = first(body, "resp_desc", "fail_reason", "failure_reason");
+ return StringUtils.hasText(message) ? message : "斗拱未返回终态说明";
+ }
+
+ private String generateWithdrawalNo() {
+ return "WD" + LocalDateTime.now().format(NO_TIME_FORMAT)
+ + String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
+ }
+
+ private String maskCard(String cardNumber) {
+ if (!StringUtils.hasText(cardNumber)) return "";
+ String value = cardNumber.trim();
+ return "****" + value.substring(Math.max(0, value.length() - 4));
+ }
+
+ private BigDecimal money(BigDecimal value) {
+ return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private boolean isSuccess(String status) {
+ return "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)
+ || "SUCCEEDED".equalsIgnoreCase(status);
+ }
+
+ private boolean isFailure(JSONObject body, String status) {
+ if ("F".equalsIgnoreCase(status) || "FAIL".equalsIgnoreCase(status)
+ || "FAILED".equalsIgnoreCase(status)) {
+ return true;
+ }
+ String responseCode = first(body, "resp_code", "code");
+ return StringUtils.hasText(responseCode)
+ && !"00000000".equals(responseCode)
+ && !"00000100".equals(responseCode);
+ }
+
+ private String first(JSONObject body, String... keys) {
+ if (body == null) return null;
+ for (String key : keys) {
+ String value = body.getString(key);
+ if (StringUtils.hasText(value)) return value;
+ }
+ JSONObject data = body.getJSONObject("data");
+ return data == null ? null : first(data, keys);
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalStateService.java b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalStateService.java
new file mode 100644
index 0000000..e04df87
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalStateService.java
@@ -0,0 +1,198 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.modules.finance.dto.WithdrawalResponse;
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ThreadLocalRandom;
+
+/** 管理提现资金流程的本地事务状态,不在数据库事务中调用外部资金接口。 */
+@Service
+public class GuideWithdrawalStateService {
+ static final String PENDING = "PENDING";
+ static final String TRANSFERRING = "TRANSFERRING";
+ static final String TRANSFERRED = "TRANSFERRED";
+ static final String PROCESSING = "PROCESSING";
+ static final String APPROVED = "APPROVED";
+ static final String REJECTED = "REJECTED";
+
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
+ private static final DateTimeFormatter SEQ_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
+
+ private final GuideWithdrawalMapper withdrawalMapper;
+ private final GuideWithdrawableBalanceService balanceService;
+
+ public GuideWithdrawalStateService(GuideWithdrawalMapper withdrawalMapper,
+ GuideWithdrawableBalanceService balanceService) {
+ this.withdrawalMapper = withdrawalMapper;
+ this.balanceService = balanceService;
+ }
+
+ @Transactional
+ public WithdrawalResponse reject(Long id, String remark, String processor) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!PENDING.equals(withdrawal.getStatus())) throw new BusinessException(409, "提现申请已处理");
+ if (!StringUtils.hasText(remark)) throw new BusinessException(400, "驳回提现时请填写处理备注");
+ balanceService.credit(withdrawal.getGuideId(), withdrawal.getAmount());
+ withdrawal.setStatus(REJECTED);
+ withdrawal.setAuditRemark(remark.trim());
+ withdrawal.setProcessor(processor);
+ withdrawal.setProcessedAt(LocalDateTime.now());
+ update(withdrawal);
+ return new WithdrawalResponse(withdrawal);
+ }
+
+ @Transactional
+ public GuideWithdrawal prepareTransfer(Long id, String remark, String processor) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!PENDING.equals(withdrawal.getStatus())) throw new BusinessException(409, "提现申请已处理");
+ LocalDateTime now = LocalDateTime.now();
+ withdrawal.setStatus(TRANSFERRING);
+ withdrawal.setTransferReqSeqId(sequence("TF"));
+ withdrawal.setTransferReqDate(now.format(DATE_FORMAT));
+ withdrawal.setTransferHfSeqId(null);
+ withdrawal.setPayoutMessage("等待主账户划账");
+ withdrawal.setAuditRemark(StringUtils.hasText(remark) ? remark.trim() : null);
+ withdrawal.setProcessor(processor);
+ withdrawal.setProcessedAt(now);
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markTransferred(Long id, String hfSeqId) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!TRANSFERRING.equals(withdrawal.getStatus())) return withdrawal;
+ withdrawal.setStatus(TRANSFERRED);
+ withdrawal.setTransferHfSeqId(hfSeqId);
+ withdrawal.setTransferredAt(LocalDateTime.now());
+ withdrawal.setPayoutMessage("主账户划账成功,等待发起取现");
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markTransferPending(Long id, String hfSeqId, String message) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!TRANSFERRING.equals(withdrawal.getStatus())) return withdrawal;
+ if (StringUtils.hasText(hfSeqId)) withdrawal.setTransferHfSeqId(hfSeqId);
+ withdrawal.setPayoutMessage(limit(message));
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markTransferFailed(Long id, String message) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!TRANSFERRING.equals(withdrawal.getStatus())) return withdrawal;
+ withdrawal.setStatus(PENDING);
+ withdrawal.setPayoutMessage(limit(message));
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal prepareCash(Long id) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!TRANSFERRED.equals(withdrawal.getStatus())) return null;
+ LocalDateTime now = LocalDateTime.now();
+ withdrawal.setStatus(PROCESSING);
+ withdrawal.setCashReqSeqId(sequence("CW"));
+ withdrawal.setCashReqDate(now.format(DATE_FORMAT));
+ withdrawal.setCashHfSeqId(null);
+ withdrawal.setPayoutMessage("已发起导游账户取现");
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal resumeCashQuery(Long id) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!TRANSFERRED.equals(withdrawal.getStatus())
+ || !StringUtils.hasText(withdrawal.getCashReqSeqId())
+ || !StringUtils.hasText(withdrawal.getCashReqDate())) {
+ return null;
+ }
+ withdrawal.setStatus(PROCESSING);
+ withdrawal.setPayoutMessage("检测到原取现流水,等待查询最终状态");
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markCashPending(Long id, String hfSeqId, String message) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!PROCESSING.equals(withdrawal.getStatus())) return withdrawal;
+ if (StringUtils.hasText(hfSeqId)) withdrawal.setCashHfSeqId(hfSeqId);
+ withdrawal.setPayoutMessage(limit(message));
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markCashRetry(Long id, String message) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!PROCESSING.equals(withdrawal.getStatus())) return withdrawal;
+ LocalDateTime now = LocalDateTime.now();
+ String retryMessage = limit(message);
+ if (withdrawalMapper.resetCashForRetry(id, retryMessage, now, now) != 1) {
+ throw new BusinessException(500, "提现状态更新失败");
+ }
+ withdrawal.setStatus(TRANSFERRED);
+ withdrawal.setCashReqSeqId(null);
+ withdrawal.setCashReqDate(null);
+ withdrawal.setCashHfSeqId(null);
+ withdrawal.setPayoutMessage(retryMessage);
+ withdrawal.setProcessedAt(now);
+ withdrawal.setUpdatedAt(now);
+ return withdrawal;
+ }
+
+ @Transactional
+ public GuideWithdrawal markApproved(Long id, String hfSeqId) {
+ GuideWithdrawal withdrawal = requireForUpdate(id);
+ if (!PROCESSING.equals(withdrawal.getStatus())) return withdrawal;
+ withdrawal.setStatus(APPROVED);
+ if (StringUtils.hasText(hfSeqId)) withdrawal.setCashHfSeqId(hfSeqId);
+ withdrawal.setPayoutMessage("取现到账成功");
+ withdrawal.setProcessedAt(LocalDateTime.now());
+ update(withdrawal);
+ return withdrawal;
+ }
+
+ public GuideWithdrawal find(Long id) {
+ GuideWithdrawal withdrawal = withdrawalMapper.selectById(id);
+ if (withdrawal == null) throw new BusinessException(404, "提现申请不存在");
+ return withdrawal;
+ }
+
+ private GuideWithdrawal requireForUpdate(Long id) {
+ GuideWithdrawal withdrawal = withdrawalMapper.selectForUpdate(id);
+ if (withdrawal == null) throw new BusinessException(404, "提现申请不存在");
+ return withdrawal;
+ }
+
+ private void update(GuideWithdrawal withdrawal) {
+ withdrawal.setUpdatedAt(LocalDateTime.now());
+ if (withdrawalMapper.updateById(withdrawal) != 1) {
+ throw new BusinessException(500, "提现状态更新失败");
+ }
+ }
+
+ private String sequence(String prefix) {
+ return prefix + LocalDateTime.now().format(SEQ_TIME_FORMAT)
+ + String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
+ }
+
+ private String limit(String message) {
+ if (!StringUtils.hasText(message)) return "斗拱资金处理结果待确认";
+ String value = message.trim();
+ return value.length() <= 255 ? value : value.substring(0, 255);
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/common/huifu/biz/DouGongBasicDataBizTest.java b/backend/src/test/java/com/tourguide/common/huifu/biz/DouGongBasicDataBizTest.java
new file mode 100644
index 0000000..a1c1764
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/common/huifu/biz/DouGongBasicDataBizTest.java
@@ -0,0 +1,37 @@
+package com.tourguide.common.huifu.biz;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DouGongBasicDataBizTest {
+
+ @Test
+ void availableBalanceShouldReturnZeroWhenAccountDataIsMissing() {
+ assertThat(DouGongBasicDataBiz.availableBalance(null)).isEqualByComparingTo(BigDecimal.ZERO);
+ assertThat(DouGongBasicDataBiz.availableBalance(Collections.<String, Object>emptyMap()))
+ .isEqualByComparingTo(BigDecimal.ZERO);
+
+ Map<String, Object> emptyAccounts = new HashMap<String, Object>();
+ emptyAccounts.put("acctInfo_list", "[]");
+ assertThat(DouGongBasicDataBiz.availableBalance(emptyAccounts)).isEqualByComparingTo(BigDecimal.ZERO);
+
+ Map<String, Object> missingBalance = new HashMap<String, Object>();
+ missingBalance.put("acctInfo_list", "[{}]");
+ assertThat(DouGongBasicDataBiz.availableBalance(missingBalance)).isEqualByComparingTo(BigDecimal.ZERO);
+ }
+
+ @Test
+ void availableBalanceShouldReadAvailableAmount() {
+ Map<String, Object> response = new HashMap<String, Object>();
+ response.put("acctInfo_list", "[{\"avail_bal\":\"123.45\"}]");
+
+ assertThat(DouGongBasicDataBiz.availableBalance(response))
+ .isEqualByComparingTo(new BigDecimal("123.45"));
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideBalanceSettlementServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideBalanceSettlementServiceTest.java
new file mode 100644
index 0000000..5986e49
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideBalanceSettlementServiceTest.java
@@ -0,0 +1,66 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.modules.order.entity.TourOrder;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GuideBalanceSettlementServiceTest {
+ private TourOrderMapper orderMapper;
+ private GuideWithdrawableBalanceService balanceService;
+ private GuideBalanceSettlementService service;
+
+ @BeforeEach
+ void setUp() {
+ orderMapper = mock(TourOrderMapper.class);
+ balanceService = mock(GuideWithdrawableBalanceService.class);
+ service = new GuideBalanceSettlementService(orderMapper, balanceService);
+ }
+
+ @Test
+ void shouldCreditDueOrderAndMarkSettled() {
+ LocalDateTime now = LocalDateTime.of(2026, 8, 17, 12, 0);
+ TourOrder order = dueOrder(now.minusMinutes(1));
+ when(orderMapper.selectForUpdate(10L)).thenReturn(order);
+ when(orderMapper.updateById(order)).thenReturn(1);
+
+ assertThat(service.settle(10L, now)).isTrue();
+
+ verify(balanceService).credit(8L, new BigDecimal("90.00"));
+ verify(orderMapper).updateById(order);
+ assertThat(order.getSettledAt()).isEqualTo(now);
+ }
+
+ @Test
+ void shouldNotCreditOrderTwice() {
+ LocalDateTime now = LocalDateTime.of(2026, 8, 17, 12, 0);
+ TourOrder order = dueOrder(now.minusMinutes(1));
+ order.setSettledAt(now.minusSeconds(1));
+ when(orderMapper.selectForUpdate(10L)).thenReturn(order);
+
+ assertThat(service.settle(10L, now)).isFalse();
+
+ verify(balanceService, never()).credit(8L, new BigDecimal("90.00"));
+ verify(orderMapper, never()).updateById(order);
+ }
+
+ private TourOrder dueOrder(LocalDateTime dueAt) {
+ TourOrder order = new TourOrder();
+ order.setId(10L);
+ order.setGuideId(8L);
+ order.setStatus("COMPLETED");
+ order.setRefundStatus("NONE");
+ order.setGuideEarningAmount(new BigDecimal("90.00"));
+ order.setSettlementDueAt(dueAt);
+ return order;
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideSettlementServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideSettlementServiceTest.java
new file mode 100644
index 0000000..22e039c
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideSettlementServiceTest.java
@@ -0,0 +1,262 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.common.huifu.biz.DouGongBasicDataBiz;
+import com.tourguide.modules.finance.dto.GuideBankCardRequest;
+import com.tourguide.modules.finance.dto.GuideBankCardResponse;
+import com.tourguide.modules.finance.dto.GuideSettlementOnboardingRequest;
+import com.tourguide.modules.finance.dto.GuideSettlementStatusResponse;
+import com.tourguide.modules.finance.entity.GuideBankBranch;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalAccountMapper;
+import com.tourguide.modules.finance.mapper.GuideBankBranchMapper;
+import com.tourguide.modules.user.entity.GuideUser;
+import com.tourguide.modules.user.service.GuideUserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GuideSettlementServiceTest {
+ private GuideWithdrawalAccountMapper accountMapper;
+ private GuideUserService guideUserService;
+ private DouGongBasicDataBiz douGongBasicDataBiz;
+ private GuideBankBranchMapper bankBranchMapper;
+ private GuideSettlementService service;
+
+ @BeforeEach
+ void setUp() {
+ accountMapper = mock(GuideWithdrawalAccountMapper.class);
+ guideUserService = mock(GuideUserService.class);
+ douGongBasicDataBiz = mock(DouGongBasicDataBiz.class);
+ bankBranchMapper = mock(GuideBankBranchMapper.class);
+ service = new GuideSettlementService(accountMapper, guideUserService, douGongBasicDataBiz,
+ bankBranchMapper);
+ GuideUser guide = new GuideUser();
+ guide.setId(8L);
+ guide.setMobile("13800138000");
+ when(guideUserService.findEnabledById(8L)).thenReturn(guide);
+ doAnswer(invocation -> {
+ GuideWithdrawalAccount account = invocation.getArgument(0);
+ account.setId(10);
+ return 1;
+ }).when(accountMapper).insert(any(GuideWithdrawalAccount.class));
+ }
+
+ @Test
+ void submitShouldRouteEnterpriseToEnterpriseOnboarding() throws Exception {
+ when(accountMapper.selectOne(any())).thenReturn(null);
+ when(douGongBasicDataBiz.userBasicDataEnt(any())).thenReturn(success());
+
+ GuideSettlementStatusResponse response = service.submit(8L, enterpriseRequest());
+
+ assertThat(response.getStatus()).isEqualTo("pending");
+ assertThat(response.getSubjectType()).isEqualTo("enterprise");
+ verify(douGongBasicDataBiz).userBasicDataEnt(any(GuideWithdrawalAccount.class));
+ }
+
+ @Test
+ void submitShouldRouteIndividualToIndividualOnboarding() throws Exception {
+ when(accountMapper.selectOne(any())).thenReturn(null);
+ when(douGongBasicDataBiz.userBasicDataIndv(any())).thenReturn(individualSuccess());
+
+ GuideSettlementStatusResponse response = service.submit(8L, individualRequest());
+
+ assertThat(response.getStatus()).isEqualTo("approved");
+ assertThat(response.getSubjectType()).isEqualTo("individual");
+ verify(douGongBasicDataBiz).userBasicDataIndv(any(GuideWithdrawalAccount.class));
+ }
+
+ @Test
+ void statusShouldPromoteLegacyIndividualWithoutApplicationNo() throws Exception {
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setId(10);
+ account.setSubjectType("individual");
+ account.setHuifuId("HF100");
+ account.setIsAudit(1);
+ account.setApplicationStatus("submitted");
+ when(accountMapper.selectOne(any())).thenReturn(account);
+
+ GuideSettlementStatusResponse response = service.status(8L);
+
+ assertThat(response.getStatus()).isEqualTo("approved");
+ assertThat(account.getApplicationStatus()).isEqualTo("succeeded");
+ verify(douGongBasicDataBiz, never()).userApplyQuery(any(), any());
+ }
+
+ @Test
+ void bindBankCardShouldOnlySaveCanonicalBranchDataLocally() throws Exception {
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setId(10);
+ account.setIsAudit(2);
+ account.setHuifuId("HF100");
+ account.setCertificateName("张三");
+ account.setCertificateCode("510000199001010000");
+ account.setCertificateExpireTime("2020-01-01,长期");
+ when(accountMapper.selectCurrentForUpdate(anyInt())).thenReturn(account);
+
+ GuideBankBranch branch = new GuideBankBranch();
+ branch.setBankName("数据库标准银行");
+ branch.setBranchName("数据库标准支行");
+ branch.setBranchCode("105510000001");
+ branch.setCityCode("510100");
+ when(bankBranchMapper.selectOne(any())).thenReturn(branch);
+ GuideBankCardRequest request = bankCardRequest();
+ request.setBankName("前端伪造银行");
+ request.setBranchName("前端伪造支行");
+ GuideBankCardResponse response = service.bindBankCard(8L, request);
+
+ assertThat(account.getBankName()).isEqualTo("数据库标准银行");
+ assertThat(account.getBankBranchName()).isEqualTo("数据库标准支行");
+ assertThat(account.getBankBranchCode()).isEqualTo("105510000001");
+ assertThat(account.getBankCardCityCode()).isEqualTo("510100");
+ assertThat(account.getBankCardStatus()).isEqualTo("local");
+ assertThat(account.getTokenNo()).isNull();
+ assertThat(response.getStatus()).isEqualTo("local");
+ verify(douGongBasicDataBiz, never()).userBusinessOpen(any());
+ }
+
+ @Test
+ void openBankCardBusinessShouldCallHuifuWhenCardIsSelected() throws Exception {
+ GuideWithdrawalAccount account = approvedAccountWithLocalCard();
+ when(accountMapper.selectCurrentForUpdate(anyInt())).thenReturn(account);
+ Map<String, Object> result = success();
+ result.put("token_no", "CARD_TOKEN_100");
+ when(douGongBasicDataBiz.userBusinessOpen(any())).thenReturn(result);
+
+ GuideBankCardResponse response = service.openBankCardBusiness(8L, 10);
+
+ assertThat(response.getStatus()).isEqualTo("approved");
+ assertThat(account.getTokenNo()).isEqualTo("CARD_TOKEN_100");
+ assertThat(account.getBankCardStatus()).isEqualTo("approved");
+ verify(douGongBasicDataBiz).userBusinessOpen(account);
+ }
+
+ @Test
+ void openBankCardBusinessShouldBeIdempotentAfterApproval() throws Exception {
+ GuideWithdrawalAccount account = approvedAccountWithLocalCard();
+ account.setTokenNo("CARD_TOKEN_100");
+ account.setBankCardStatus("approved");
+ when(accountMapper.selectCurrentForUpdate(anyInt())).thenReturn(account);
+
+ GuideBankCardResponse response = service.openBankCardBusiness(8L, 10);
+
+ assertThat(response.getStatus()).isEqualTo("approved");
+ verify(douGongBasicDataBiz, never()).userBusinessOpen(any());
+ }
+
+ @Test
+ void openBankCardBusinessShouldOnlyRefreshExistingPendingApplication() throws Exception {
+ GuideWithdrawalAccount account = approvedAccountWithLocalCard();
+ account.setBankCardStatus("pending");
+ account.setCusApplicationId("APPLY100");
+ when(accountMapper.selectCurrentForUpdate(anyInt())).thenReturn(account);
+ when(douGongBasicDataBiz.userApplyQuery(account, "APPLY100")).thenReturn(success());
+
+ GuideBankCardResponse response = service.openBankCardBusiness(8L, 10);
+
+ assertThat(response.getStatus()).isEqualTo("pending");
+ verify(douGongBasicDataBiz, never()).userBusinessOpen(any());
+ verify(douGongBasicDataBiz).userApplyQuery(account, "APPLY100");
+ }
+
+ @Test
+ void resubmitShouldNotChangeExistingSubjectType() {
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setId(10);
+ account.setSubjectType("individual");
+ account.setHuifuId("HF100");
+ account.setIsAudit(3);
+ account.setApplicationStatus("failed");
+ when(accountMapper.selectOne(any())).thenReturn(account);
+
+ assertThatThrownBy(() -> service.submit(8L, enterpriseRequest()))
+ .hasMessage("已提交的进件主体类型不可变更,请按原类型重新提交");
+ }
+
+ private GuideSettlementOnboardingRequest enterpriseRequest() {
+ GuideSettlementOnboardingRequest request = baseRequest("enterprise");
+ request.setEnterpriseName("成都名导文旅有限公司");
+ request.setCreditCode("91510100MA1234567X");
+ request.setLicenseValidityType("fixed");
+ request.setLicenseBeginDate("2020-01-01");
+ request.setLicenseEndDate("2040-01-01");
+ request.setRegisteredProvinceCode("510000");
+ request.setRegisteredCityCode("510100");
+ request.setRegisteredDistrictCode("510107");
+ request.setRegisteredAddress("四川省成都市武侯区测试路1号");
+ return request;
+ }
+
+ private GuideSettlementOnboardingRequest individualRequest() {
+ return baseRequest("individual");
+ }
+
+ private GuideSettlementOnboardingRequest baseRequest(String subjectType) {
+ GuideSettlementOnboardingRequest request = new GuideSettlementOnboardingRequest();
+ request.setSubjectType(subjectType);
+ request.setSubjectName("张三");
+ request.setSubjectIdNumber("510000199001010000");
+ request.setSubjectCertificateValidityType("long_term");
+ request.setSubjectCertificateBeginDate("2020-01-01");
+ request.setContactName("张三");
+ request.setContactMobile("13800138000");
+ return request;
+ }
+
+ private GuideBankCardRequest bankCardRequest() {
+ GuideBankCardRequest request = new GuideBankCardRequest();
+ request.setCardNumber("6222021234567890");
+ request.setProvinceCode("510000");
+ request.setCityCode("510100");
+ request.setBankName("测试银行");
+ request.setBranchName("测试支行");
+ request.setBranchCode("105510000001");
+ return request;
+ }
+
+ private GuideWithdrawalAccount approvedAccountWithLocalCard() {
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setId(10);
+ account.setIsAudit(2);
+ account.setHuifuId("HF100");
+ account.setCertificateName("张三");
+ account.setCertificateCode("510000199001010000");
+ account.setCertificateExpireTime("2020-01-01,长期");
+ account.setBankName("数据库标准银行");
+ account.setBankCardCode("6222021234567890");
+ account.setBankBranchName("数据库标准支行");
+ account.setBankBranchCode("105510000001");
+ account.setBankCardProvinceCode("510000");
+ account.setBankCardCityCode("510100");
+ account.setOpenAccountName("张三");
+ account.setBankCardStatus("local");
+ return account;
+ }
+
+ private Map<String, Object> success() {
+ Map<String, Object> result = new HashMap<String, Object>();
+ result.put("resp_code", "00000000");
+ result.put("huifu_id", "HF100");
+ result.put("apply_no", "APPLY100");
+ return result;
+ }
+
+ private Map<String, Object> individualSuccess() {
+ Map<String, Object> result = new HashMap<String, Object>();
+ result.put("resp_code", "00000000");
+ result.put("resp_desc", "成功");
+ result.put("huifu_id", "HF100");
+ return result;
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceServiceTest.java
new file mode 100644
index 0000000..e57ee69
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawableBalanceServiceTest.java
@@ -0,0 +1,38 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.modules.user.mapper.GuideUserMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class GuideWithdrawableBalanceServiceTest {
+ private GuideUserMapper guideUserMapper;
+ private GuideWithdrawableBalanceService service;
+
+ @BeforeEach
+ void setUp() {
+ guideUserMapper = mock(GuideUserMapper.class);
+ service = new GuideWithdrawableBalanceService(guideUserMapper);
+ }
+
+ @Test
+ void shouldReadStoredGuideBalance() {
+ when(guideUserMapper.selectBalance(8L)).thenReturn(new BigDecimal("504.30"));
+
+ assertThat(service.getAvailableAmount(8L)).isEqualByComparingTo("504.30");
+ }
+
+ @Test
+ void shouldAtomicallyDebitStoredBalance() {
+ when(guideUserMapper.decreaseBalance(8L, new BigDecimal("80.00"))).thenReturn(1);
+
+ assertThat(service.debit(8L, new BigDecimal("80"))).isTrue();
+ verify(guideUserMapper).decreaseBalance(8L, new BigDecimal("80.00"));
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutServiceTest.java
new file mode 100644
index 0000000..51ea68a
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalPayoutServiceTest.java
@@ -0,0 +1,151 @@
+package com.tourguide.modules.finance.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.tourguide.common.huifu.biz.DouGongWithdrawBiz;
+import com.tourguide.common.huifu.biz.FenZhangUtil;
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GuideWithdrawalPayoutServiceTest {
+ private GuideWithdrawalStateService stateService;
+ private GuideSettlementService settlementService;
+ private FenZhangUtil fenZhangUtil;
+ private DouGongWithdrawBiz withdrawBiz;
+ private GuideWithdrawalPayoutService service;
+
+ @BeforeEach
+ void setUp() {
+ stateService = mock(GuideWithdrawalStateService.class);
+ settlementService = mock(GuideSettlementService.class);
+ fenZhangUtil = mock(FenZhangUtil.class);
+ withdrawBiz = mock(DouGongWithdrawBiz.class);
+ service = new GuideWithdrawalPayoutService(
+ stateService, settlementService, fenZhangUtil, withdrawBiz);
+ }
+
+ @Test
+ void approveShouldTransferFromPlatformBeforeSubmittingCash() throws Exception {
+ GuideWithdrawal transferring = withdrawal("TRANSFERRING");
+ transferring.setTransferReqSeqId("TF001");
+ transferring.setTransferReqDate("20260817");
+ GuideWithdrawal cash = withdrawal("PROCESSING");
+ cash.setCashReqSeqId("CW001");
+ cash.setCashReqDate("20260817");
+ GuideWithdrawalAccount account = account();
+ when(stateService.prepareTransfer(20L, null, "admin")).thenReturn(transferring);
+ when(stateService.prepareCash(20L)).thenReturn(cash);
+ when(stateService.find(20L)).thenReturn(cash);
+ when(settlementService.requireApprovedCard(8L, 10)).thenReturn(account);
+ Map<String, Object> transferResponse = new HashMap<>();
+ transferResponse.put("resp_code", "00000000");
+ transferResponse.put("trans_stat", "S");
+ transferResponse.put("hf_seq_id", "HF-TF-1");
+ when(fenZhangUtil.fenzhang("TF001", "20260817", "200.00", "HF100"))
+ .thenReturn(transferResponse);
+ JSONObject cashResponse = new JSONObject();
+ cashResponse.put("resp_code", "00000100");
+ cashResponse.put("resp_desc", "受理成功");
+ when(withdrawBiz.encashment(new BigDecimal("200.00"), "HF100", "TOKEN100",
+ "CW001", "20260817")).thenReturn(cashResponse);
+
+ service.approve(20L, null, "admin");
+
+ verify(stateService).markTransferred(20L, "HF-TF-1");
+ verify(withdrawBiz).encashment(new BigDecimal("200.00"), "HF100", "TOKEN100",
+ "CW001", "20260817");
+ }
+
+ @Test
+ void transferFailureShouldNeverSubmitCash() throws Exception {
+ GuideWithdrawal transferring = withdrawal("TRANSFERRING");
+ transferring.setTransferReqSeqId("TF001");
+ transferring.setTransferReqDate("20260817");
+ when(stateService.prepareTransfer(20L, null, "admin")).thenReturn(transferring);
+ when(stateService.find(20L)).thenReturn(transferring);
+ when(settlementService.requireApprovedCard(8L, 10)).thenReturn(account());
+ Map<String, Object> response = new HashMap<>();
+ response.put("resp_code", "100001");
+ response.put("resp_desc", "主账户余额不足");
+ when(fenZhangUtil.fenzhang("TF001", "20260817", "200.00", "HF100"))
+ .thenReturn(response);
+
+ service.approve(20L, null, "admin");
+
+ verify(stateService).markTransferFailed(20L, "主账户划账失败:主账户余额不足");
+ verify(withdrawBiz, never()).encashment(
+ org.mockito.ArgumentMatchers.any(), anyString(), anyString(), anyString(), anyString());
+ }
+
+ @Test
+ void transferredRecordWithCashSequenceShouldQueryOriginalRequestInsteadOfResubmitting() {
+ GuideWithdrawal transferred = withdrawal("TRANSFERRED");
+ transferred.setCashReqSeqId("CW001");
+ transferred.setCashReqDate("20260818");
+ GuideWithdrawal processing = withdrawal("PROCESSING");
+ processing.setCashReqSeqId("CW001");
+ processing.setCashReqDate("20260818");
+ when(stateService.find(20L)).thenReturn(transferred);
+ when(stateService.resumeCashQuery(20L)).thenReturn(processing);
+ when(settlementService.requireApprovedCard(8L, 10)).thenReturn(account());
+ JSONObject queryResponse = new JSONObject();
+ queryResponse.put("resp_code", "00000000");
+ queryResponse.put("trans_stat", "S");
+ when(withdrawBiz.query("HF100", "20260818", "CW001")).thenReturn(queryResponse);
+
+ service.advance(20L);
+
+ verify(stateService).resumeCashQuery(20L);
+ verify(withdrawBiz).query("HF100", "20260818", "CW001");
+ verify(withdrawBiz, never()).encashment(any(), anyString(), anyString(), anyString(), anyString());
+ verify(stateService).markApproved(20L, null);
+ }
+
+ @Test
+ void cashQueryBusinessErrorShouldRemainProcessing() {
+ GuideWithdrawal processing = withdrawal("PROCESSING");
+ processing.setCashReqSeqId("CW001");
+ processing.setCashReqDate("20260818");
+ when(stateService.find(20L)).thenReturn(processing);
+ when(settlementService.requireApprovedCard(8L, 10)).thenReturn(account());
+ JSONObject queryResponse = new JSONObject();
+ queryResponse.put("resp_code", "20000100");
+ queryResponse.put("resp_desc", "查询服务暂不可用");
+ when(withdrawBiz.query("HF100", "20260818", "CW001")).thenReturn(queryResponse);
+
+ service.advance(20L);
+
+ verify(stateService).markCashPending(20L, null,
+ "导游账户取现查询失败:查询服务暂不可用");
+ verify(stateService, never()).markCashRetry(any(), anyString());
+ }
+
+ private GuideWithdrawal withdrawal(String status) {
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setId(20L);
+ withdrawal.setGuideId(8L);
+ withdrawal.setBankCardId(10);
+ withdrawal.setAmount(new BigDecimal("200.00"));
+ withdrawal.setStatus(status);
+ return withdrawal;
+ }
+
+ private GuideWithdrawalAccount account() {
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setHuifuId("HF100");
+ account.setTokenNo("TOKEN100");
+ return account;
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalServiceTest.java
new file mode 100644
index 0000000..3cc91d7
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalServiceTest.java
@@ -0,0 +1,173 @@
+package com.tourguide.modules.finance.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.tourguide.modules.finance.dto.GuideWithdrawalCreateRequest;
+import com.tourguide.modules.finance.dto.WithdrawalResponse;
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.entity.GuideWithdrawalAccount;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper;
+import com.tourguide.modules.user.entity.GuideUser;
+import com.tourguide.modules.user.service.GuideUserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.never;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class GuideWithdrawalServiceTest {
+ private GuideWithdrawalMapper withdrawalMapper;
+ private GuideSettlementService settlementService;
+ private GuideUserService guideUserService;
+ private GuideWithdrawableBalanceService balanceService;
+ private GuideWithdrawalStateService stateService;
+ private GuideWithdrawalPayoutService payoutService;
+ private GuideWithdrawalService service;
+
+ @BeforeEach
+ void setUp() {
+ withdrawalMapper = mock(GuideWithdrawalMapper.class);
+ settlementService = mock(GuideSettlementService.class);
+ guideUserService = mock(GuideUserService.class);
+ balanceService = mock(GuideWithdrawableBalanceService.class);
+ stateService = mock(GuideWithdrawalStateService.class);
+ payoutService = mock(GuideWithdrawalPayoutService.class);
+ service = new GuideWithdrawalService(withdrawalMapper, settlementService,
+ guideUserService, balanceService, stateService, payoutService);
+ }
+
+ @Test
+ void createShouldReserveAvailableBalanceAndPersistCardSnapshot() throws Exception {
+ GuideUser guide = new GuideUser();
+ guide.setRealName("张三");
+ guide.setMobile("13800138000");
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setId(10);
+ account.setHuifuId("HF100");
+ account.setBankName("招商银行");
+ account.setBankCardCode("6225888888885678");
+ when(guideUserService.findEnabledById(8L)).thenReturn(guide);
+ when(settlementService.requireApprovedCardForUpdate(8L, 10)).thenReturn(account);
+ when(balanceService.debit(8L, new BigDecimal("200.00"))).thenReturn(true);
+ doAnswer(invocation -> {
+ GuideWithdrawal withdrawal = invocation.getArgument(0);
+ withdrawal.setId(20L);
+ return 1;
+ }).when(withdrawalMapper).insert(any(GuideWithdrawal.class));
+ GuideWithdrawalCreateRequest request = new GuideWithdrawalCreateRequest();
+ request.setAmount(new BigDecimal("200.00"));
+ request.setBankCardId(10);
+
+ WithdrawalResponse response = service.create(8L, request);
+
+ assertThat(response.getAmount()).isEqualByComparingTo("200.00");
+ assertThat(response.getBankName()).isEqualTo("招商银行");
+ assertThat(response.getCardMasked()).isEqualTo("****5678");
+ assertThat(response.getStatus()).isEqualTo("PENDING");
+ verify(balanceService).debit(8L, new BigDecimal("200.00"));
+ }
+
+ @Test
+ void rejectShouldReturnReservedBalance() {
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setId(20L);
+ withdrawal.setGuideId(8L);
+ withdrawal.setAmount(new BigDecimal("200.00"));
+ withdrawal.setStatus("REJECTED");
+ when(stateService.reject(20L, "资料不完整", "admin"))
+ .thenReturn(new WithdrawalResponse(withdrawal));
+
+ WithdrawalResponse response = service.process(20L, "REJECTED", "资料不完整", "admin");
+
+ assertThat(response.getStatus()).isEqualTo("REJECTED");
+ verify(stateService).reject(20L, "资料不完整", "admin");
+ }
+
+ @Test
+ void failedCashCallbackShouldReturnToTransferredWithoutRefundingLocalBalance() {
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setId(20L);
+ withdrawal.setGuideId(8L);
+ withdrawal.setAmount(new BigDecimal("200.00"));
+ withdrawal.setCashReqSeqId("CW001");
+ withdrawal.setCashReqDate("20260817");
+ withdrawal.setStatus("PROCESSING");
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setHuifuId("HF100");
+ when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal);
+ when(settlementService.findCurrent(8L)).thenReturn(account);
+ when(withdrawalMapper.resetCashForRetry(any(), any(), any(), any())).thenReturn(1);
+ JSONObject body = new JSONObject();
+ body.put("req_seq_id", "CW001");
+ body.put("huifu_id", "HF100");
+ body.put("cash_amt", "200.00");
+ body.put("trans_stat", "F");
+ body.put("resp_desc", "银行处理中断");
+
+ assertThat(service.handleCallback(body)).isTrue();
+
+ assertThat(withdrawal.getStatus()).isEqualTo("TRANSFERRED");
+ assertThat(withdrawal.getCashReqSeqId()).isNull();
+ verify(balanceService, org.mockito.Mockito.never()).credit(8L, new BigDecimal("200.00"));
+ }
+
+ @Test
+ void nonTerminalCashCallbackShouldRemainProcessing() {
+ GuideWithdrawal withdrawal = processingWithdrawal();
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setHuifuId("HF100");
+ when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal);
+ when(settlementService.findCurrent(8L)).thenReturn(account);
+ when(withdrawalMapper.updateById(withdrawal)).thenReturn(1);
+ JSONObject body = new JSONObject();
+ body.put("req_seq_id", "CW001");
+ body.put("huifu_id", "HF100");
+ body.put("cash_amt", "200.00");
+ body.put("hf_seq_id", "HF-CASH-1");
+
+ assertThat(service.handleCallback(body)).isTrue();
+
+ assertThat(withdrawal.getStatus()).isEqualTo("PROCESSING");
+ assertThat(withdrawal.getCashReqSeqId()).isEqualTo("CW001");
+ assertThat(withdrawal.getCashHfSeqId()).isEqualTo("HF-CASH-1");
+ verify(withdrawalMapper, never()).resetCashForRetry(any(), any(), any(), any());
+ }
+
+ @Test
+ void successCallbackShouldRecoverTransferredRecordWithSameCashSequence() {
+ GuideWithdrawal withdrawal = processingWithdrawal();
+ withdrawal.setStatus("TRANSFERRED");
+ GuideWithdrawalAccount account = new GuideWithdrawalAccount();
+ account.setHuifuId("HF100");
+ when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal);
+ when(settlementService.findCurrent(8L)).thenReturn(account);
+ when(withdrawalMapper.updateById(withdrawal)).thenReturn(1);
+ JSONObject body = new JSONObject();
+ body.put("req_seq_id", "CW001");
+ body.put("huifu_id", "HF100");
+ body.put("cash_amt", "200.00");
+ body.put("trans_stat", "S");
+
+ assertThat(service.handleCallback(body)).isTrue();
+
+ assertThat(withdrawal.getStatus()).isEqualTo("APPROVED");
+ assertThat(withdrawal.getPayoutMessage()).isEqualTo("取现到账成功");
+ }
+
+ private GuideWithdrawal processingWithdrawal() {
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setId(20L);
+ withdrawal.setGuideId(8L);
+ withdrawal.setAmount(new BigDecimal("200.00"));
+ withdrawal.setCashReqSeqId("CW001");
+ withdrawal.setCashReqDate("20260817");
+ withdrawal.setStatus("PROCESSING");
+ return withdrawal;
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalStateServiceTest.java b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalStateServiceTest.java
new file mode 100644
index 0000000..2ed7a8d
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/finance/service/GuideWithdrawalStateServiceTest.java
@@ -0,0 +1,79 @@
+package com.tourguide.modules.finance.service;
+
+import com.tourguide.modules.finance.entity.GuideWithdrawal;
+import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GuideWithdrawalStateServiceTest {
+ private GuideWithdrawalMapper withdrawalMapper;
+ private GuideWithdrawableBalanceService balanceService;
+ private GuideWithdrawalStateService service;
+
+ @BeforeEach
+ void setUp() {
+ withdrawalMapper = mock(GuideWithdrawalMapper.class);
+ balanceService = mock(GuideWithdrawableBalanceService.class);
+ service = new GuideWithdrawalStateService(withdrawalMapper, balanceService);
+ }
+
+ @Test
+ void rejectShouldRefundReservedLocalBalance() {
+ GuideWithdrawal withdrawal = pendingWithdrawal();
+ when(withdrawalMapper.selectForUpdate(20L)).thenReturn(withdrawal);
+ when(withdrawalMapper.updateById(withdrawal)).thenReturn(1);
+
+ service.reject(20L, "资料不完整", "admin");
+
+ verify(balanceService).credit(8L, new BigDecimal("200.00"));
+ assertThat(withdrawal.getStatus()).isEqualTo("REJECTED");
+ }
+
+ @Test
+ void prepareTransferShouldPersistIdempotentRequestIdentity() {
+ GuideWithdrawal withdrawal = pendingWithdrawal();
+ when(withdrawalMapper.selectForUpdate(20L)).thenReturn(withdrawal);
+ when(withdrawalMapper.updateById(withdrawal)).thenReturn(1);
+
+ service.prepareTransfer(20L, null, "admin");
+
+ assertThat(withdrawal.getStatus()).isEqualTo("TRANSFERRING");
+ assertThat(withdrawal.getTransferReqSeqId()).startsWith("TF");
+ assertThat(withdrawal.getTransferReqDate()).hasSize(8);
+ assertThat(withdrawal.getUpdatedAt()).isNotNull();
+ }
+
+ @Test
+ void cashFailureShouldClearPersistedRequestIdentity() {
+ GuideWithdrawal withdrawal = pendingWithdrawal();
+ withdrawal.setStatus("PROCESSING");
+ withdrawal.setCashReqSeqId("CW001");
+ withdrawal.setCashReqDate("20260817");
+ withdrawal.setCashHfSeqId("HF001");
+ when(withdrawalMapper.selectForUpdate(20L)).thenReturn(withdrawal);
+ when(withdrawalMapper.resetCashForRetry(any(), any(), any(), any())).thenReturn(1);
+
+ service.markCashRetry(20L, "取现失败");
+
+ verify(withdrawalMapper).resetCashForRetry(any(), any(), any(), any());
+ assertThat(withdrawal.getStatus()).isEqualTo("TRANSFERRED");
+ assertThat(withdrawal.getCashReqSeqId()).isNull();
+ }
+
+ private GuideWithdrawal pendingWithdrawal() {
+ GuideWithdrawal withdrawal = new GuideWithdrawal();
+ withdrawal.setId(20L);
+ withdrawal.setGuideId(8L);
+ withdrawal.setAmount(new BigDecimal("200.00"));
+ withdrawal.setStatus("PENDING");
+ return withdrawal;
+ }
+}
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.js
new file mode 100644
index 0000000..35c1346
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),a=require("../../common/assets.js"),r={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js"},data:()=>({cityName:"",cityCode:"",searchKeyword:"",branches:[],loading:!1,loadError:!1,searchTimer:null,isLeaving:!1}),onLoad(e){e&&e.city&&(this.cityName=decodeURIComponent(e.city)),e&&e.cityCode&&(this.cityCode=decodeURIComponent(e.cityCode)),this.loadBranches()},onUnload(){this.searchTimer&&clearTimeout(this.searchTimer)},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/bank-card-add/index")},280))},onSearchInput(){this.searchTimer&&clearTimeout(this.searchTimer),this.searchTimer=setTimeout(()=>{this.loadBranches()},250)},loadBranches(){this.loading=!0,this.loadError=!1,e.searchBankBranches(this.searchKeyword.trim(),this.cityCode).then(e=>{this.branches=e||[],this.loading=!1}).catch(()=>{this.loading=!1,this.loadError=!0})},selectBranch(a){e.saveSelectedBranch({branchName:a.branchName,bankName:a.bankName,branchCode:a.branchCode}),e.index.navigateBack()}}};if(!Array){e.resolveComponent("guide-nav-bar")()}Math;const o=e._export_sfc(r,[["render",function(r,o,n,t,i,c){return e.e({a:e.o(c.goBack,"bb"),b:e.p({title:"选择开户行"}),c:e.o([e=>i.searchKeyword=e.detail.value,(...e)=>c.onSearchInput&&c.onSearchInput(...e)],"42"),d:i.searchKeyword,e:a._imports_0$6,f:e.t(i.cityName),g:e.f(i.branches,(a,r,o)=>e.e({a:e.t(a.branchName),b:r<i.branches.length-1},(i.branches.length,{}),{c:a.id,d:e.o(e=>c.selectBranch(a),a.id)})),h:a._imports_0$2,i:i.loading},i.loading?{}:i.loadError?{k:e.o((...e)=>c.loadBranches&&c.loadBranches(...e),"25")}:(i.branches.length,{}),{j:i.loadError,l:!i.branches.length,m:i.isLeaving?1:""})}],["__scopeId","data-v-2d3b58a9"]]);wx.createPage(o);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.json
new file mode 100644
index 0000000..f316f72
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "选择开户行",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxml
new file mode 100644
index 0000000..f4c7aa4
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxml
@@ -0,0 +1 @@
+<view class="{{['page', 'data-v-2d3b58a9', m && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-2d3b58a9" bindback="{{a}}" u-i="2d3b58a9-0" bind:__l="__l" u-p="{{b}}"/><view class="search-section data-v-2d3b58a9"><view class="search-box data-v-2d3b58a9"><input class="search-input data-v-2d3b58a9" maxlength="30" placeholder="搜索银行或支行名称" placeholder-class="search-placeholder" bindinput="{{c}}" value="{{d}}"/><image class="search-icon data-v-2d3b58a9" src="{{e}}" mode="aspectFit"/></view><text class="city-tip data-v-2d3b58a9">当前开户城市:{{f}}</text></view><scroll-view class="branch-scroll data-v-2d3b58a9" scroll-y><view class="branch-card data-v-2d3b58a9"><view wx:for="{{g}}" wx:for-item="branch" wx:key="c" class="branch-item data-v-2d3b58a9" bindtap="{{branch.d}}"><text class="branch-name data-v-2d3b58a9">{{branch.a}}</text><image class="branch-arrow data-v-2d3b58a9" src="{{h}}" mode="aspectFit"/><view wx:if="{{branch.b}}" class="branch-divider data-v-2d3b58a9"></view></view></view><view wx:if="{{i}}" class="empty-state data-v-2d3b58a9"><text class="empty-title data-v-2d3b58a9">正在加载开户行</text></view><view wx:elif="{{j}}" class="empty-state data-v-2d3b58a9" bindtap="{{k}}"><text class="empty-title data-v-2d3b58a9">开户行加载失败</text><text class="empty-tip data-v-2d3b58a9">点击重新加载</text></view><view wx:elif="{{l}}" class="empty-state data-v-2d3b58a9"><text class="empty-title data-v-2d3b58a9">未找到匹配的开户行</text><text class="empty-tip data-v-2d3b58a9">请尝试输入银行简称或支行名称</text></view><view class="safe-space data-v-2d3b58a9"></view></scroll-view></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxss
new file mode 100644
index 0000000..b511336
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-branch-select/index.wxss
@@ -0,0 +1 @@
+.page.data-v-2d3b58a9{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding:0;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-2d3b58a9 .26s cubic-bezier(.22,1,.36,1) both}.page.leaving.data-v-2d3b58a9{animation:pageSlideOut-2d3b58a9 .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-2d3b58a9{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-2d3b58a9{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.search-section.data-v-2d3b58a9{padding:28rpx 28rpx 24rpx;flex:none;box-sizing:border-box}.search-box.data-v-2d3b58a9{display:flex;height:88rpx;padding:0 24rpx;flex-direction:row;align-items:center;border:1rpx solid #cfd5dd;border-radius:12rpx;background:#fff;box-sizing:border-box}.search-input.data-v-2d3b58a9{flex:1;height:86rpx;color:#1d2939;font-size:27rpx}.search-placeholder.data-v-2d3b58a9{color:#a8b1be;font-size:27rpx}.search-icon.data-v-2d3b58a9{width:38rpx;height:38rpx;margin-left:18rpx}.city-tip.data-v-2d3b58a9{display:block;margin:18rpx 8rpx 0;color:#667085;font-size:23rpx}.branch-scroll.data-v-2d3b58a9{flex:1;min-height:0;padding:0 28rpx;box-sizing:border-box}.branch-card.data-v-2d3b58a9{overflow:hidden;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff}.branch-item.data-v-2d3b58a9{display:flex;min-height:104rpx;padding:0 26rpx;flex-direction:row;align-items:center;position:relative;box-sizing:border-box}.branch-name.data-v-2d3b58a9{flex:1;padding:26rpx 0;color:#1d2939;font-size:27rpx}.branch-arrow.data-v-2d3b58a9{width:30rpx;height:30rpx;margin-left:18rpx;opacity:.58}.branch-divider.data-v-2d3b58a9{height:1rpx;position:absolute;left:26rpx;right:26rpx;bottom:0;background:#edf0f4}.empty-state.data-v-2d3b58a9{display:flex;padding:110rpx 30rpx;flex-direction:column;align-items:center}.empty-title.data-v-2d3b58a9{color:#4b5667;font-size:28rpx;font-weight:700}.empty-tip.data-v-2d3b58a9{margin-top:12rpx;color:#9aa4b2;font-size:24rpx}.safe-space.data-v-2d3b58a9{height:calc(60rpx + env(safe-area-inset-bottom))}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.js
new file mode 100644
index 0000000..6f7381a
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),t=require("../../common/assets.js"),r={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js",GuideDialog:()=>"../../components/guide-dialog/guide-dialog.js"},data:()=>({areaProvinceNames:e.areaProvinceNames,submitting:!1,cardNumber:"",selectedProvince:"",selectedProvinceCode:"",selectedCity:"",selectedCityCode:"",currentCityNames:[],currentCityCodes:[],selectedBranch:"",selectedBankName:"",selectedBranchCode:"",cardNumberError:"",cityError:"",branchError:"",confirmVisible:!1,isLeaving:!1}),computed:{confirmContent(){const e=this.cardNumber.trim();return this.selectedBranch+"\n尾号 "+e.slice(-4)}},onShow(){const t=e.consumeSelectedBranch();t&&(this.selectedBranch=t.branchName||"",this.selectedBankName=t.bankName||"",this.selectedBranchCode=t.branchCode||"",this.branchError="")},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/bank-card-select/index")},280))},showCardTypeTip(){e.index.showToast({title:"当前仅支持本人银行卡",icon:"none"})},onProvinceChange(t){const r=parseInt(t.detail.value,10);this.selectedProvince=e.areaProvinceNames[r],this.selectedProvinceCode=e.areaProvinceCodes[r],this.currentCityNames=e.areaCityNames[r]||[],this.currentCityCodes=e.areaCityCodes[r]||[],this.selectedCity="",this.selectedCityCode="",this.selectedBranch="",this.selectedBranchCode="",this.cityError="",this.branchError=""},onCityChange(e){const t=parseInt(e.detail.value,10);this.selectedCity=this.currentCityNames[t],this.selectedCityCode=this.currentCityCodes[t],this.selectedBranch="",this.selectedBranchCode="",this.cityError="",this.branchError=""},handleCityRowTap(){this.selectedProvince||e.index.showToast({title:"请先选择开户省份",icon:"none"})},chooseBranch(){if(this.branchError="",!this.selectedCity)return this.cityError="请先选择开户城市",void e.index.showToast({title:"请先选择开户城市",icon:"none"});e.routeTo("/pages/bank-branch-select/index?city="+encodeURIComponent(this.selectedProvince+" "+this.selectedCity)+"&cityCode="+encodeURIComponent(this.selectedCityCode))},submitBankCard(){if(this.submitting)return;this.cardNumberError="",this.cityError="",this.branchError="";let e=!1;const t=this.cardNumber.trim();t?/^\d{16,19}$/.test(t)||(this.cardNumberError="请输入 16 至 19 位银行卡号",e=!0):(this.cardNumberError="请输入银行卡号",e=!0),this.selectedCity||(this.cityError="请选择开户城市",e=!0),this.selectedProvinceCode&&this.selectedCityCode||(this.cityError="请选择有效的省份和城市",e=!0),this.selectedBranch&&this.selectedBranchCode||(this.branchError="请选择开户行",e=!0),e||(this.confirmVisible=!0)},confirmAdd(){this.submitting||(this.submitting=!0,e.addBankCard({cardNumber:this.cardNumber.trim(),provinceCode:this.selectedProvinceCode,cityCode:this.selectedCityCode,bankName:this.selectedBankName,branchName:this.selectedBranch,branchCode:this.selectedBranchCode}).then(()=>{this.submitting=!1,e.index.showToast({title:"银行卡已添加",icon:"success"}),setTimeout(()=>{e.index.navigateBack()},500)}).catch(t=>{this.submitting=!1,e.index.showToast({title:t&&t.message||"提交失败",icon:"none"})}))}}};if(!Array){(e.resolveComponent("guide-nav-bar")+e.resolveComponent("guide-dialog"))()}Math||((()=>"../../components/guide-nav-bar/guide-nav-bar.js")+(()=>"../../components/guide-dialog/guide-dialog.js"))();const i=e._export_sfc(r,[["render",function(r,i,s,o,c,n){return e.e({a:e.o(n.goBack,"bb"),b:e.p({title:"填写银行卡"}),c:t._imports_0$4,d:e.o((...e)=>n.showCardTypeTip&&n.showCardTypeTip(...e),"f1"),e:e.o([e=>c.cardNumber=e.detail.value,e=>c.cardNumberError=""],"26"),f:c.cardNumber,g:c.cardNumberError?1:"",h:c.cardNumberError},c.cardNumberError?{i:e.t(c.cardNumberError)}:{},{j:e.t(c.selectedProvince||"选择开户省份"),k:c.selectedProvince?"":1,l:t._imports_0$4,m:c.cityError&&!c.selectedProvince?1:"",n:c.areaProvinceNames,o:e.o((...e)=>n.onProvinceChange&&n.onProvinceChange(...e),"ba"),p:e.t(c.selectedCity||"选择开户城市"),q:c.selectedCity?"":1,r:t._imports_0$4,s:c.cityError&&c.selectedProvince?1:"",t:c.currentCityNames,v:!c.selectedProvince,w:e.o((...e)=>n.onCityChange&&n.onCityChange(...e),"3f"),x:e.o((...e)=>n.handleCityRowTap&&n.handleCityRowTap(...e),"a3"),y:c.cityError},c.cityError?{z:e.t(c.cityError)}:{},{A:e.t(c.selectedBranch||"选择开户行"),B:c.selectedBranch?"":1,C:t._imports_0$4,D:c.branchError?1:"",E:e.o((...e)=>n.chooseBranch&&n.chooseBranch(...e),"84"),F:c.branchError},c.branchError?{G:e.t(c.branchError)}:{},{H:e.t(c.submitting?"提交中":"添加银行卡"),I:c.submitting?1:"",J:e.o((...e)=>n.submitBankCard&&n.submitBankCard(...e),"e6"),K:e.o(e=>c.confirmVisible=!1,"4e"),L:e.o(n.confirmAdd,"a3"),M:e.p({visible:c.confirmVisible,title:"确认添加银行卡",content:n.confirmContent,"confirm-text":"确认添加"}),N:c.isLeaving?1:""})}],["__scopeId","data-v-76554add"]]);wx.createPage(i);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.json
new file mode 100644
index 0000000..77d0376
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.json
@@ -0,0 +1,10 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "填写银行卡",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar",
+ "guide-dialog": "../../components/guide-dialog/guide-dialog"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxml
new file mode 100644
index 0000000..05ff9f5
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxml
@@ -0,0 +1 @@
+<view class="{{['page', 'data-v-76554add', N && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-76554add" bindback="{{a}}" u-i="76554add-0" bind:__l="__l" u-p="{{b}}"/><scroll-view class="form-scroll data-v-76554add" scroll-y><view class="form-card data-v-76554add"><view class="form-row data-v-76554add" bindtap="{{d}}"><text class="field-label data-v-76554add">卡类型</text><view class="field-control readonly-control data-v-76554add"><text class="field-value data-v-76554add">本人</text><image class="down-icon data-v-76554add" src="{{c}}" mode="aspectFit"/></view></view><view class="row-divider data-v-76554add"></view><view class="field-block data-v-76554add"><view class="form-row data-v-76554add"><text class="field-label data-v-76554add">卡号</text><view class="{{['field-control', 'input-control', 'data-v-76554add', g && 'field-invalid']}}"><input class="card-input data-v-76554add" type="number" maxlength="19" placeholder="请输入银行卡号" placeholder-class="input-placeholder" bindinput="{{e}}" value="{{f}}"/></view></view><text wx:if="{{h}}" class="field-error data-v-76554add">{{i}}</text></view><view class="row-divider data-v-76554add"></view><view class="field-block data-v-76554add"><view class="form-row data-v-76554add"><text class="field-label data-v-76554add">开户省份</text><picker class="region-picker data-v-76554add" mode="selector" range="{{n}}" bindchange="{{o}}"><view class="{{['field-control', 'data-v-76554add', m && 'field-invalid']}}"><text class="{{['field-value', 'data-v-76554add', k && 'field-placeholder']}}">{{j}}</text><image class="down-icon data-v-76554add" src="{{l}}" mode="aspectFit"/></view></picker></view></view><view class="row-divider data-v-76554add"></view><view class="field-block data-v-76554add"><view class="form-row data-v-76554add" bindtap="{{x}}"><text class="field-label data-v-76554add">开户城市</text><picker class="region-picker data-v-76554add" mode="selector" range="{{t}}" disabled="{{v}}" bindchange="{{w}}"><view class="{{['field-control', 'data-v-76554add', s && 'field-invalid']}}"><text class="{{['field-value', 'data-v-76554add', q && 'field-placeholder']}}">{{p}}</text><image class="down-icon data-v-76554add" src="{{r}}" mode="aspectFit"/></view></picker></view><text wx:if="{{y}}" class="field-error data-v-76554add">{{z}}</text></view><view class="row-divider data-v-76554add"></view><view class="field-block data-v-76554add"><view class="form-row data-v-76554add" bindtap="{{E}}"><text class="field-label data-v-76554add">开户行</text><view class="{{['field-control', 'data-v-76554add', D && 'field-invalid']}}"><text class="{{['field-value', 'branch-value', 'data-v-76554add', B && 'field-placeholder']}}">{{A}}</text><image class="down-icon data-v-76554add" src="{{C}}" mode="aspectFit"/></view></view><text wx:if="{{F}}" class="field-error data-v-76554add">{{G}}</text></view></view><view class="security-tip data-v-76554add"><text class="data-v-76554add">银行卡仅用于导游提现,请填写本人名下银行卡</text></view><view class="{{['submit-button', 'data-v-76554add', I && 'disabled']}}" bindtap="{{J}}"><text class="data-v-76554add">{{H}}</text></view><view class="safe-space data-v-76554add"></view></scroll-view><guide-dialog wx:if="{{M}}" class="data-v-76554add" bindcancel="{{K}}" bindconfirm="{{L}}" u-i="76554add-1" bind:__l="__l" u-p="{{M}}"/></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxss
new file mode 100644
index 0000000..acc1b29
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-add/index.wxss
@@ -0,0 +1 @@
+.page.data-v-76554add{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding:0;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-76554add .26s cubic-bezier(.22,1,.36,1) both}.page.leaving.data-v-76554add{animation:pageSlideOut-76554add .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-76554add{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-76554add{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.form-scroll.data-v-76554add{flex:1;min-height:0;padding:28rpx 28rpx 0;box-sizing:border-box}.form-card.data-v-76554add{padding:8rpx 30rpx;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff;box-sizing:border-box}.form-row.data-v-76554add{display:flex;min-height:112rpx;flex-direction:row;align-items:center}.field-block.data-v-76554add{display:flex;flex-direction:column}.field-label.data-v-76554add{width:150rpx;flex:none;color:#1f3b5b;font-size:28rpx;font-weight:700}.field-control.data-v-76554add{display:flex;min-height:76rpx;padding:0 22rpx;flex:1;flex-direction:row;align-items:center;border:1rpx solid #cfd5dd;border-radius:12rpx;background:#fff;box-sizing:border-box}.region-picker.data-v-76554add{flex:1}.readonly-control.data-v-76554add{background:#f0f2f5;border-color:#f0f2f5}.input-control.data-v-76554add{padding-right:14rpx}.field-value.data-v-76554add{flex:1;color:#1d2939;font-size:27rpx}.branch-value.data-v-76554add{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-placeholder.data-v-76554add{color:#a8b1be}.card-input.data-v-76554add{flex:1;height:74rpx;color:#1d2939;font-size:27rpx}.input-placeholder.data-v-76554add{color:#a8b1be;font-size:27rpx}.down-icon.data-v-76554add{width:30rpx;height:30rpx;margin-left:12rpx;flex:none}.row-divider.data-v-76554add{height:1rpx;margin-left:150rpx;background:#eef1f5}.field-invalid.data-v-76554add{border-color:#f0746d;background:#fff9f8}.field-error.data-v-76554add{margin:-8rpx 0 14rpx 150rpx;color:#e95b55;font-size:23rpx}.security-tip.data-v-76554add{display:flex;margin:24rpx 16rpx 0;color:#8e98a7;font-size:23rpx;line-height:34rpx}.submit-button.data-v-76554add{display:flex;width:100%;height:88rpx;margin-top:58rpx;justify-content:center;align-items:center;border-radius:12rpx;color:#fff;font-size:29rpx;font-weight:700;background:#4d8fe8}.submit-button.disabled.data-v-76554add{opacity:.58}.safe-space.data-v-76554add{height:calc(70rpx + env(safe-area-inset-bottom))}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.js
new file mode 100644
index 0000000..e7bd94d
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),a={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js"},data:()=>({loading:!1,cards:[],openingId:"",isLeaving:!1}),onShow(){this.load()},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/withdrawal-detail/index")},280))},addCard(){e.routeTo("/pages/bank-card-add/index")},statusText:e=>"approved"===e?"已通过":"pending"===e?"入驻中":"rejected"===e?"入驻失败":"待入驻",selectCard(a){this.openingId||("approved"!==a.status?(this.openingId=a.id,e.openBankCardBusiness(a.id).then(a=>{this.replaceCard(a),a&&"approved"===a.status?this.finishSelection(a):e.index.showToast({title:a&&a.reason||"银行卡业务入驻处理中",icon:"none"})}).catch(a=>{e.index.showToast({title:a&&a.message||"银行卡业务入驻失败",icon:"none"}),this.load()}).finally(()=>{this.openingId=""})):this.finishSelection(a))},finishSelection(a){e.saveSelectedBankId(a.id),e.index.navigateBack()},replaceCard(e){if(!e)return;const a=this.cards.findIndex(a=>a.id===e.id);a<0||this.cards.splice(a,1,e)},load(){this.loading=!0,e.getBankCards().then(e=>{this.cards=e||[]}).catch(a=>{e.index.showToast({title:a&&a.message||"银行卡加载失败",icon:"none"})}).finally(()=>{this.loading=!1})}}};if(!Array){e.resolveComponent("guide-nav-bar")()}Math;const n=e._export_sfc(a,[["render",function(a,n,t,i,s,d){return e.e({a:e.o(d.goBack,"bb"),b:e.p({title:"选择银行卡"}),c:e.o((...e)=>d.addCard&&d.addCard(...e),"bc"),d:s.loading},(s.loading||s.cards.length,{}),{e:!s.cards.length,f:e.f(s.cards,(a,n,t)=>e.e({a:e.t(a.bankName),b:e.t(s.openingId===a.id?"入驻中":d.statusText(a.status)),c:e.n("status "+a.status),d:e.t(a.cardMasked),e:e.t(a.branchName),f:a.reason},a.reason?{g:e.t(a.reason)}:{},{h:a.id,i:e.o(e=>d.selectCard(a),a.id)})),g:s.isLeaving?1:""})}],["__scopeId","data-v-cca320b2"]]);wx.createPage(n);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.json
new file mode 100644
index 0000000..b847662
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "选择银行卡",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxml
new file mode 100644
index 0000000..898da19
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxml
@@ -0,0 +1 @@
+<view class="{{['page', 'data-v-cca320b2', g && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-cca320b2" bindback="{{a}}" u-i="cca320b2-0" bind:__l="__l" u-p="{{b}}"/><view class="add data-v-cca320b2" bindtap="{{c}}">添加银行卡</view><scroll-view class="scroll data-v-cca320b2" scroll-y><view wx:if="{{d}}" class="state data-v-cca320b2">正在加载银行卡</view><view wx:elif="{{e}}" class="state data-v-cca320b2">暂无银行卡</view><view wx:for="{{f}}" wx:for-item="card" wx:key="h" class="card data-v-cca320b2" bindtap="{{card.i}}"><view class="row data-v-cca320b2"><text class="name data-v-cca320b2">{{card.a}}</text><text class="{{['data-v-cca320b2', card.c]}}">{{card.b}}</text></view><text class="number data-v-cca320b2">{{card.d}}</text><text class="branch data-v-cca320b2">{{card.e}}</text><text wx:if="{{card.f}}" class="reason data-v-cca320b2">{{card.g}}</text></view></scroll-view></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxss
new file mode 100644
index 0000000..bceb68b
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/bank-card-select/index.wxss
@@ -0,0 +1 @@
+.page.data-v-cca320b2{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding:0;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-cca320b2 .26s cubic-bezier(.22,1,.36,1) both}.page.leaving.data-v-cca320b2{animation:pageSlideOut-cca320b2 .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-cca320b2{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-cca320b2{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.add.data-v-cca320b2{height:88rpx;margin:28rpx 28rpx 24rpx;border-radius:12rpx;background:#4d8fe8;color:#fff;text-align:center;line-height:88rpx;font-weight:700}.scroll.data-v-cca320b2{flex:1;min-height:0;padding:0 28rpx;box-sizing:border-box}.card.data-v-cca320b2{padding:28rpx;margin-bottom:16rpx;border:1rpx solid #edf0f4;background:#fff;border-radius:12rpx}.row.data-v-cca320b2{display:flex;justify-content:space-between}.name.data-v-cca320b2{font-size:30rpx;font-weight:700;color:#1f3b5b}.number.data-v-cca320b2{display:block;margin-top:20rpx;font-size:28rpx;color:#344054}.branch.data-v-cca320b2,.reason.data-v-cca320b2{display:block;margin-top:10rpx;color:#667085;font-size:23rpx}.reason.data-v-cca320b2{color:#e95b5b}.status.data-v-cca320b2{font-size:23rpx}.approved.data-v-cca320b2{color:#249765}.pending.data-v-cca320b2{color:#e58b28}.local.data-v-cca320b2{color:#667085}.rejected.data-v-cca320b2{color:#e95b5b}.state.data-v-cca320b2{padding:150rpx 0;text-align:center;color:#8993a1}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.js
new file mode 100644
index 0000000..41734b2
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),t={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js"},data:()=>({status:"pending",subjectType:"individual",reason:"",refreshing:!1,isLeaving:!1}),computed:{isRejected(){return"rejected"===this.status},resultTitle(){return this.isRejected?"进件审核未通过":"approved"===this.status?"进件审核已通过":"进件信息审核中"},resultTip(){return"approved"===this.status?"审核已通过,可以继续添加银行卡和提现":"审核通过后才能继续添加银行卡和提现"},statusIcon(){return this.isRejected?"/static/icons/message-warning.svg":"approved"===this.status?"/static/icons/shield-check.svg":"/static/icons/clock-3.svg"}},onShow(){this.loadStatus()},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/withdrawal/index")},280))},loadStatus(){e.getOnboardingStatus().then(e=>{this.status=e&&e.status||"pending",this.subjectType=e&&"enterprise"===e.subjectType?"enterprise":"individual",this.reason=e&&e.reason||""})},refreshStatus(){this.refreshing||this.isRejected||(this.refreshing=!0,e.refreshOnboardingStatus().then(t=>{this.refreshing=!1,this.status=t&&t.status||this.status,this.reason=t&&t.reason||"","approved"===this.status?e.index.showToast({title:"审核已通过",icon:"success"}):e.index.showToast({title:"仍在审核中",icon:"none"})}).catch(()=>{this.refreshing=!1,e.index.showToast({title:"刷新失败,请重试",icon:"none"})}))},resubmit(){e.routeTo("/pages/onboarding/index?subjectType="+encodeURIComponent(this.subjectType))},goWithdraw(){e.routeTo("/pages/withdrawal-detail/index")}}};if(!Array){e.resolveComponent("guide-nav-bar")()}Math;const s=e._export_sfc(t,[["render",function(t,s,i,a,r,n){return e.e({a:e.o(n.goBack,"d8"),b:e.p({title:"导游进件"}),c:n.statusIcon,d:n.isRejected?1:"",e:"approved"===r.status?1:"",f:e.t(n.resultTitle),g:n.isRejected&&r.reason},n.isRejected&&r.reason?{h:e.t(r.reason)}:{i:e.t(n.resultTip)},{j:n.isRejected},n.isRejected?{k:e.o((...e)=>n.resubmit&&n.resubmit(...e),"16")}:"approved"===r.status?{m:e.o((...e)=>n.goWithdraw&&n.goWithdraw(...e),"b3")}:{n:e.t(r.refreshing?"刷新中":"刷新审核状态"),o:r.refreshing?1:"",p:e.o((...e)=>n.refreshStatus&&n.refreshStatus(...e),"da")},{l:"approved"===r.status,q:n.isRejected||"approved"===r.status?1:"",r:e.o((...e)=>n.goBack&&n.goBack(...e),"40"),s:r.isLeaving?1:""})}],["__scopeId","data-v-ad0c063f"]]);wx.createPage(s);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.json
new file mode 100644
index 0000000..c2e2944
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "导游进件",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxml
new file mode 100644
index 0000000..1ab5ba0
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxml
@@ -0,0 +1 @@
+<view class="{{['page', 'data-v-ad0c063f', s && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-ad0c063f" bindback="{{a}}" u-i="ad0c063f-0" bind:__l="__l" u-p="{{b}}"/><view class="result-content data-v-ad0c063f"><view class="{{['status-icon-wrap', 'data-v-ad0c063f', d && 'rejected', e && 'approved']}}"><image class="status-icon data-v-ad0c063f" src="{{c}}" mode="aspectFit"/></view><text class="result-title data-v-ad0c063f">{{f}}</text><text wx:if="{{g}}" class="result-reason data-v-ad0c063f">{{h}}</text><text wx:else class="result-tip data-v-ad0c063f">{{i}}</text><view wx:if="{{j}}" class="return-button data-v-ad0c063f" bindtap="{{k}}"><text class="data-v-ad0c063f">重新提交资料</text></view><view wx:elif="{{l}}" class="return-button data-v-ad0c063f" bindtap="{{m}}"><text class="data-v-ad0c063f">去提现</text></view><view wx:else class="{{['return-button', 'ghost', 'data-v-ad0c063f', o && 'disabled']}}" bindtap="{{p}}"><text class="data-v-ad0c063f">{{n}}</text></view><view class="{{['return-button', 'data-v-ad0c063f', q && 'secondary']}}" bindtap="{{r}}"><text class="data-v-ad0c063f">返回提现</text></view></view></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxss
new file mode 100644
index 0000000..bf4ebde
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding-pending/index.wxss
@@ -0,0 +1 @@
+.page.data-v-ad0c063f{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding:0;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-ad0c063f .26s cubic-bezier(.22,1,.36,1) both}.page.leaving.data-v-ad0c063f{animation:pageSlideOut-ad0c063f .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-ad0c063f{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-ad0c063f{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.result-content.data-v-ad0c063f{display:flex;padding:76rpx 48rpx 0;flex:1;flex-direction:column;align-items:center;box-sizing:border-box}.status-icon-wrap.data-v-ad0c063f{display:flex;width:176rpx;height:176rpx;justify-content:center;align-items:center;border-radius:88rpx;background:#eef6ff}.status-icon-wrap.rejected.data-v-ad0c063f{background:#fff1f2}.status-icon-wrap.approved.data-v-ad0c063f{background:#eafaf0}.status-icon.data-v-ad0c063f{width:82rpx;height:82rpx}.result-title.data-v-ad0c063f{margin-top:40rpx;color:#1f3b5b;font-size:34rpx;font-weight:700}.result-tip.data-v-ad0c063f,.result-reason.data-v-ad0c063f{margin-top:16rpx;color:#667085;font-size:25rpx;text-align:center;line-height:1.5}.result-reason.data-v-ad0c063f{color:#e95b5b}.return-button.data-v-ad0c063f{display:flex;width:100%;height:88rpx;margin-top:28rpx;justify-content:center;align-items:center;border-radius:12rpx;color:#fff;font-size:29rpx;font-weight:700;background:#4d8fe8}.return-button.ghost.data-v-ad0c063f{margin-top:70rpx;color:#4d8fe8;background:#fff;box-shadow:none;border:1rpx solid #4d8fe8}.return-button.secondary.data-v-ad0c063f{margin-top:24rpx;color:#4d8fe8;background:#fff;box-shadow:none;border:1rpx solid #4d8fe8}.return-button.disabled.data-v-ad0c063f{opacity:.58}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.js
new file mode 100644
index 0000000..f58111d
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),i=require("../../common/assets.js"),t={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js",GuideDialog:()=>"../../components/guide-dialog/guide-dialog.js"},data:()=>({submitting:!1,merchantType:"enterprise",enterpriseName:"",creditCode:"",businessValidity:"固定期限",businessStartDate:"",businessEndDate:"",registeredRegion:"",registeredProvinceCode:"",registeredCityCode:"",registeredDistrictCode:"",registeredAddress:"",subjectName:"",subjectIdNumber:"",subjectValidity:"固定期限",subjectStartDate:"",subjectEndDate:"",administratorName:"",administratorPhone:"",personalPhone:"",invalidField:"",errorMessage:"",confirmVisible:!1,isLeaving:!1,merchantTypeOptions:["企业","个人"],validityOptions:["固定期限","长期"]}),onLoad(e){!e||"individual"!==e.subjectType&&"enterprise"!==e.subjectType||(this.merchantType=e.subjectType)},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/withdrawal/index")},280))},setFieldError(e,i){this.invalidField=e,this.errorMessage=i},clearFieldError(e){this.invalidField===e&&(this.invalidField="",this.errorMessage="")},normalizeCreditCode(e){const i=e&&e.detail&&e.detail.value||this.creditCode;this.creditCode=String(i||"").toUpperCase().replace(/[^0-9A-Z]/g,""),this.clearFieldError("creditCode")},chooseMerchantType(){e.index.showActionSheet({itemList:this.merchantTypeOptions,success:e=>{this.merchantType=0===e.tapIndex?"enterprise":"individual",this.invalidField="",this.errorMessage=""}})},chooseBusinessValidity(){e.index.showActionSheet({itemList:this.validityOptions,success:e=>{this.businessValidity=this.validityOptions[e.tapIndex],"长期"===this.businessValidity&&(this.businessEndDate=""),this.clearFieldError("businessValidity")}})},chooseSubjectValidity(){e.index.showActionSheet({itemList:this.validityOptions,success:e=>{this.subjectValidity=this.validityOptions[e.tapIndex],"长期"===this.subjectValidity&&(this.subjectEndDate=""),this.clearFieldError("subjectValidity")}})},onRegisteredRegionChange(e){const i=e.detail&&e.detail.value||[],t=e.detail&&e.detail.code||[];3===i.length&&(this.registeredRegion=i[0]+" "+i[1]+" "+i[2],this.registeredProvinceCode=t[0]||"",this.registeredCityCode=t[1]||"",this.registeredDistrictCode=t[2]||"",this.clearFieldError("registeredRegion"))},onBusinessStartChange(e){this.businessStartDate=e.detail.value,this.clearFieldError("businessStartDate")},onBusinessEndChange(e){this.businessEndDate=e.detail.value,this.clearFieldError("businessEndDate")},onSubjectStartChange(e){this.subjectStartDate=e.detail.value,this.clearFieldError("subjectStartDate")},onSubjectEndChange(e){this.subjectEndDate=e.detail.value,this.clearFieldError("subjectEndDate")},isPhoneValid:e=>/^1[3-9]\d{9}$/.test(e),validateCommonIdentity(){if(!this.subjectName.trim())return this.setFieldError("subjectName","enterprise"===this.merchantType?"请输入法人姓名":"请输入个人姓名"),!1;if(!/^\d{17}[\dXx]$/.test(this.subjectIdNumber.trim()))return this.setFieldError("subjectIdNumber","请输入正确的18位身份证号码"),!1;if(!this.subjectStartDate)return this.setFieldError("subjectStartDate","请选择证件有效期开始日期"),!1;if("固定期限"===this.subjectValidity){if(!this.subjectEndDate)return this.setFieldError("subjectEndDate","请选择证件有效期截止日期"),!1;if(this.subjectStartDate>this.subjectEndDate)return this.setFieldError("subjectEndDate","证件截止日期不能早于开始日期"),!1}return!0},validateEnterprise(){if(!this.enterpriseName.trim())return this.setFieldError("enterpriseName","请输入企业名称"),!1;if(!/^[0-9A-Z]{18}$/.test(this.creditCode.trim()))return this.setFieldError("creditCode","请输入正确的18位统一社会信用代码"),!1;if(!this.businessStartDate)return this.setFieldError("businessStartDate","请选择营业执照有效期开始日期"),!1;if("固定期限"===this.businessValidity){if(!this.businessEndDate)return this.setFieldError("businessEndDate","请选择营业执照有效期结束日期"),!1;if(this.businessStartDate>this.businessEndDate)return this.setFieldError("businessEndDate","证照结束日期不能早于开始日期"),!1}return this.registeredRegion?this.registeredAddress.trim().length<5?(this.setFieldError("registeredAddress","请输入完整的详细注册地址"),!1):!!this.validateCommonIdentity()&&(this.administratorName.trim()?!!this.isPhoneValid(this.administratorPhone.trim())||(this.setFieldError("administratorPhone","请输入正确的管理员手机号"),!1):(this.setFieldError("administratorName","请输入管理员姓名"),!1)):(this.setFieldError("registeredRegion","请选择注册地址"),!1)},validateIndividual(){return!!this.validateCommonIdentity()&&(!!this.isPhoneValid(this.personalPhone.trim())||(this.setFieldError("personalPhone","请输入正确的手机号"),!1))},submitOnboarding(){if(this.submitting)return;this.invalidField="",this.errorMessage="";("enterprise"===this.merchantType?this.validateEnterprise():this.validateIndividual())?this.confirmVisible=!0:e.index.showToast({title:this.errorMessage,icon:"none"})},confirmSubmit(){if(this.submitting)return;this.submitting=!0;const i="enterprise"===this.merchantType?this.administratorName.trim():this.subjectName.trim(),t="enterprise"===this.merchantType?this.administratorPhone.trim():this.personalPhone.trim();e.submitOnboarding({subjectType:this.merchantType,enterpriseName:this.enterpriseName.trim(),subjectName:this.subjectName.trim(),creditCode:this.creditCode.trim(),subjectIdNumber:this.subjectIdNumber.trim(),subjectCertificateValidityType:"长期"===this.subjectValidity?"long_term":"fixed",subjectCertificateBeginDate:this.subjectStartDate,subjectCertificateEndDate:this.subjectEndDate,licenseValidityType:"长期"===this.businessValidity?"long_term":"fixed",licenseBeginDate:this.businessStartDate,licenseEndDate:this.businessEndDate,registeredRegion:this.registeredRegion.trim(),registeredProvinceCode:this.registeredProvinceCode,registeredCityCode:this.registeredCityCode,registeredDistrictCode:this.registeredDistrictCode,registeredAddress:this.registeredAddress.trim(),contactName:i,contactMobile:t}).then(()=>{this.submitting=!1,e.routeTo("/pages/onboarding-pending/index","redirectTo")}).catch(i=>{this.submitting=!1,e.index.showToast({title:i&&i.message||"提交失败,请重试",icon:"none"})})}}};if(!Array){(e.resolveComponent("guide-nav-bar")+e.resolveComponent("guide-dialog"))()}Math||((()=>"../../components/guide-nav-bar/guide-nav-bar.js")+(()=>"../../components/guide-dialog/guide-dialog.js"))();const s=e._export_sfc(t,[["render",function(t,s,r,a,d,n){return e.e({a:e.o(n.goBack,"d8"),b:e.p({title:"导游进件"}),c:e.t("enterprise"===d.merchantType?"企业":"个人"),d:i._imports_0$4,e:e.o((...e)=>n.chooseMerchantType&&n.chooseMerchantType(...e),"c3"),f:"enterprise"===d.merchantType},"enterprise"===d.merchantType?e.e({g:e.o([e=>d.enterpriseName=e.detail.value,e=>n.clearFieldError("enterpriseName")],"b0"),h:d.enterpriseName,i:"enterpriseName"===d.invalidField?1:"",j:"enterpriseName"===d.invalidField},"enterpriseName"===d.invalidField?{k:e.t(d.errorMessage)}:{},{l:e.o([e=>d.creditCode=e.detail.value,(...e)=>n.normalizeCreditCode&&n.normalizeCreditCode(...e)],"b0"),m:d.creditCode,n:"creditCode"===d.invalidField?1:"",o:"creditCode"===d.invalidField},"creditCode"===d.invalidField?{p:e.t(d.errorMessage)}:{},{q:e.t(d.businessValidity),r:i._imports_0$4,s:"businessValidity"===d.invalidField?1:"",t:e.o((...e)=>n.chooseBusinessValidity&&n.chooseBusinessValidity(...e),"5f"),v:"businessValidity"===d.invalidField},"businessValidity"===d.invalidField?{w:e.t(d.errorMessage)}:{},{x:e.t(d.businessStartDate||"请选择"),y:d.businessStartDate?"":1,z:i._imports_0$4,A:"businessStartDate"===d.invalidField?1:"",B:d.businessStartDate,C:e.o((...e)=>n.onBusinessStartChange&&n.onBusinessStartChange(...e),"80"),D:"businessStartDate"===d.invalidField},"businessStartDate"===d.invalidField?{E:e.t(d.errorMessage)}:{},{F:"固定期限"===d.businessValidity},"固定期限"===d.businessValidity?e.e({G:e.t(d.businessEndDate||"请选择"),H:d.businessEndDate?"":1,I:i._imports_0$4,J:"businessEndDate"===d.invalidField?1:"",K:d.businessEndDate,L:e.o((...e)=>n.onBusinessEndChange&&n.onBusinessEndChange(...e),"f0"),M:"businessEndDate"===d.invalidField},"businessEndDate"===d.invalidField?{N:e.t(d.errorMessage)}:{}):{},{O:e.t(d.registeredRegion||"请选择"),P:d.registeredRegion?"":1,Q:i._imports_0$4,R:"registeredRegion"===d.invalidField?1:"",S:e.o((...e)=>n.onRegisteredRegionChange&&n.onRegisteredRegionChange(...e),"81"),T:"registeredRegion"===d.invalidField},"registeredRegion"===d.invalidField?{U:e.t(d.errorMessage)}:{},{V:e.o([e=>d.registeredAddress=e.detail.value,e=>n.clearFieldError("registeredAddress")],"0a"),W:d.registeredAddress,X:"registeredAddress"===d.invalidField?1:"",Y:"registeredAddress"===d.invalidField},"registeredAddress"===d.invalidField?{Z:e.t(d.errorMessage)}:{}):{},{aa:e.t("enterprise"===d.merchantType?"法人信息":"个人信息"),ab:e.t("enterprise"===d.merchantType?"法人姓名":"个人姓名"),ac:e.o([e=>d.subjectName=e.detail.value,e=>n.clearFieldError("subjectName")],"c0"),ad:d.subjectName,ae:"subjectName"===d.invalidField?1:"",af:"subjectName"===d.invalidField},"subjectName"===d.invalidField?{ag:e.t(d.errorMessage)}:{},{ah:e.t("enterprise"===d.merchantType?"法人证件类型":"个人证件类型"),ai:i._imports_0$4,aj:e.t("enterprise"===d.merchantType?"法人证件号码":"个人证件号码"),ak:e.o([e=>d.subjectIdNumber=e.detail.value,e=>n.clearFieldError("subjectIdNumber")],"7c"),al:d.subjectIdNumber,am:"subjectIdNumber"===d.invalidField?1:"",an:"subjectIdNumber"===d.invalidField},"subjectIdNumber"===d.invalidField?{ao:e.t(d.errorMessage)}:{},{ap:e.t("enterprise"===d.merchantType?"法人证件有效期类型":"个人证件有效期类型"),aq:e.t(d.subjectValidity),ar:i._imports_0$4,as:"subjectValidity"===d.invalidField?1:"",at:e.o((...e)=>n.chooseSubjectValidity&&n.chooseSubjectValidity(...e),"c1"),av:"subjectValidity"===d.invalidField},"subjectValidity"===d.invalidField?{aw:e.t(d.errorMessage)}:{},{ax:e.t("enterprise"===d.merchantType?"法人证件有效期开始日期":"个人证件有效期开始日期"),ay:e.t(d.subjectStartDate||"请选择"),az:d.subjectStartDate?"":1,aA:i._imports_0$4,aB:"subjectStartDate"===d.invalidField?1:"",aC:d.subjectStartDate,aD:e.o((...e)=>n.onSubjectStartChange&&n.onSubjectStartChange(...e),"dd"),aE:"subjectStartDate"===d.invalidField},"subjectStartDate"===d.invalidField?{aF:e.t(d.errorMessage)}:{},{aG:"固定期限"===d.subjectValidity},"固定期限"===d.subjectValidity?e.e({aH:e.t("enterprise"===d.merchantType?"法人证件有效期截止日期":"个人证件有效期截止日期"),aI:e.t(d.subjectEndDate||"请选择"),aJ:d.subjectEndDate?"":1,aK:i._imports_0$4,aL:"subjectEndDate"===d.invalidField?1:"",aM:d.subjectEndDate,aN:e.o((...e)=>n.onSubjectEndChange&&n.onSubjectEndChange(...e),"2e"),aO:"subjectEndDate"===d.invalidField},"subjectEndDate"===d.invalidField?{aP:e.t(d.errorMessage)}:{}):{},{aQ:"individual"===d.merchantType},"individual"===d.merchantType?e.e({aR:e.o([e=>d.personalPhone=e.detail.value,e=>n.clearFieldError("personalPhone")],"cd"),aS:d.personalPhone,aT:"personalPhone"===d.invalidField?1:"",aU:"personalPhone"===d.invalidField},"personalPhone"===d.invalidField?{aV:e.t(d.errorMessage)}:{}):{},{aW:"enterprise"===d.merchantType},"enterprise"===d.merchantType?e.e({aX:e.o([e=>d.administratorName=e.detail.value,e=>n.clearFieldError("administratorName")],"b7"),aY:d.administratorName,aZ:"administratorName"===d.invalidField?1:"",ba:"administratorName"===d.invalidField},"administratorName"===d.invalidField?{bb:e.t(d.errorMessage)}:{},{bc:e.o([e=>d.administratorPhone=e.detail.value,e=>n.clearFieldError("administratorPhone")],"3a"),bd:d.administratorPhone,be:"administratorPhone"===d.invalidField?1:"",bf:"administratorPhone"===d.invalidField},"administratorPhone"===d.invalidField?{bg:e.t(d.errorMessage)}:{}):{},{bh:e.t(d.submitting?"提交中":"提交审核"),bi:d.submitting?1:"",bj:e.o((...e)=>n.submitOnboarding&&n.submitOnboarding(...e),"0a"),bk:e.o(e=>d.confirmVisible=!1,"e3"),bl:e.o(n.confirmSubmit,"65"),bm:e.p({visible:d.confirmVisible,title:"确认提交审核",content:"提交后资料将进入审核,审核期间暂不能发起提现。","confirm-text":"确认提交"}),bn:d.isLeaving?1:""})}],["__scopeId","data-v-dcfd67c8"]]);wx.createPage(s);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.json
new file mode 100644
index 0000000..39daca9
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.json
@@ -0,0 +1,10 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "导游进件",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar",
+ "guide-dialog": "../../components/guide-dialog/guide-dialog"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxml
new file mode 100644
index 0000000..c250fb9
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxml
@@ -0,0 +1 @@
+<view class="{{['page', 'data-v-dcfd67c8', bn && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-dcfd67c8" bindback="{{a}}" u-i="dcfd67c8-0" bind:__l="__l" u-p="{{b}}"/><scroll-view class="form-scroll data-v-dcfd67c8" scroll-y><view class="form-card subject-card data-v-dcfd67c8"><view class="merchant-type-row data-v-dcfd67c8" bindtap="{{e}}"><view class="type-copy data-v-dcfd67c8"><text class="field-label inline-label data-v-dcfd67c8">进件类型</text></view><view class="type-control data-v-dcfd67c8"><text class="data-v-dcfd67c8">{{c}}</text><image class="down-icon data-v-dcfd67c8" src="{{d}}" mode="aspectFit"/></view></view></view><view wx:if="{{f}}" class="form-card data-v-dcfd67c8"><text class="section-title data-v-dcfd67c8">企业资料</text><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">企业名称</text><view class="{{['input-shell', 'data-v-dcfd67c8', i && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" maxlength="128" placeholder="请输入营业执照上的企业名称" placeholder-class="input-placeholder" bindinput="{{g}}" value="{{h}}"/></view><text wx:if="{{j}}" class="field-error data-v-dcfd67c8">{{k}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">统一社会信用代码</text><view class="{{['input-shell', 'data-v-dcfd67c8', n && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" maxlength="18" placeholder="请输入18位统一社会信用代码" placeholder-class="input-placeholder" bindinput="{{l}}" value="{{m}}"/></view><text wx:if="{{o}}" class="field-error data-v-dcfd67c8">{{p}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">营业执照有效期</text><view class="{{['select-shell', 'data-v-dcfd67c8', s && 'field-invalid']}}" bindtap="{{t}}"><text class="data-v-dcfd67c8">{{q}}</text><image class="down-icon data-v-dcfd67c8" src="{{r}}" mode="aspectFit"/></view><text wx:if="{{v}}" class="field-error data-v-dcfd67c8">{{w}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">证照有效期起始日期</text><picker class="date-picker data-v-dcfd67c8" mode="date" value="{{B}}" start="1990-01-01" end="2099-12-31" bindchange="{{C}}"><view class="{{['select-shell', 'data-v-dcfd67c8', A && 'field-invalid']}}"><text class="{{['data-v-dcfd67c8', y && 'select-placeholder']}}">{{x}}</text><image class="down-icon data-v-dcfd67c8" src="{{z}}" mode="aspectFit"/></view></picker><text wx:if="{{D}}" class="field-error data-v-dcfd67c8">{{E}}</text></view><view wx:if="{{F}}" class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">证照有效期结束日期</text><picker class="date-picker data-v-dcfd67c8" mode="date" value="{{K}}" start="1990-01-01" end="2099-12-31" bindchange="{{L}}"><view class="{{['select-shell', 'data-v-dcfd67c8', J && 'field-invalid']}}"><text class="{{['data-v-dcfd67c8', H && 'select-placeholder']}}">{{G}}</text><image class="down-icon data-v-dcfd67c8" src="{{I}}" mode="aspectFit"/></view></picker><text wx:if="{{M}}" class="field-error data-v-dcfd67c8">{{N}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">注册地址(省、市、区)</text><picker class="region-picker data-v-dcfd67c8" mode="region" bindchange="{{S}}"><view class="{{['select-shell', 'data-v-dcfd67c8', R && 'field-invalid']}}"><text class="{{['ellipsis-text', 'data-v-dcfd67c8', P && 'select-placeholder']}}">{{O}}</text><image class="down-icon data-v-dcfd67c8" src="{{Q}}" mode="aspectFit"/></view></picker><text wx:if="{{T}}" class="field-error data-v-dcfd67c8">{{U}}</text></view><view class="field-block last-field data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">详细注册地址</text><view class="{{['input-shell', 'data-v-dcfd67c8', X && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" maxlength="80" placeholder="请输入详细注册地址" placeholder-class="input-placeholder" bindinput="{{V}}" value="{{W}}"/></view><text wx:if="{{Y}}" class="field-error data-v-dcfd67c8">{{Z}}</text></view></view><view class="form-card data-v-dcfd67c8"><text class="section-title data-v-dcfd67c8">{{aa}}</text><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{ab}}</text><view class="{{['input-shell', 'data-v-dcfd67c8', ae && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" maxlength="30" placeholder="请输入姓名" placeholder-class="input-placeholder" bindinput="{{ac}}" value="{{ad}}"/></view><text wx:if="{{af}}" class="field-error data-v-dcfd67c8">{{ag}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{ah}}</text><view class="select-shell readonly-shell data-v-dcfd67c8"><text class="data-v-dcfd67c8">身份证</text><image class="down-icon data-v-dcfd67c8" src="{{ai}}" mode="aspectFit"/></view></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{aj}}</text><view class="{{['input-shell', 'data-v-dcfd67c8', am && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" type="idcard" maxlength="18" placeholder="请输入身份证号码" placeholder-class="input-placeholder" bindinput="{{ak}}" value="{{al}}"/></view><text wx:if="{{an}}" class="field-error data-v-dcfd67c8">{{ao}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{ap}}</text><view class="{{['select-shell', 'data-v-dcfd67c8', as && 'field-invalid']}}" bindtap="{{at}}"><text class="data-v-dcfd67c8">{{aq}}</text><image class="down-icon data-v-dcfd67c8" src="{{ar}}" mode="aspectFit"/></view><text wx:if="{{av}}" class="field-error data-v-dcfd67c8">{{aw}}</text></view><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{ax}}</text><picker class="date-picker data-v-dcfd67c8" mode="date" value="{{aC}}" start="1990-01-01" end="2099-12-31" bindchange="{{aD}}"><view class="{{['select-shell', 'data-v-dcfd67c8', aB && 'field-invalid']}}"><text class="{{['data-v-dcfd67c8', az && 'select-placeholder']}}">{{ay}}</text><image class="down-icon data-v-dcfd67c8" src="{{aA}}" mode="aspectFit"/></view></picker><text wx:if="{{aE}}" class="field-error data-v-dcfd67c8">{{aF}}</text></view><view wx:if="{{aG}}" class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">{{aH}}</text><picker class="date-picker data-v-dcfd67c8" mode="date" value="{{aM}}" start="1990-01-01" end="2099-12-31" bindchange="{{aN}}"><view class="{{['select-shell', 'data-v-dcfd67c8', aL && 'field-invalid']}}"><text class="{{['data-v-dcfd67c8', aJ && 'select-placeholder']}}">{{aI}}</text><image class="down-icon data-v-dcfd67c8" src="{{aK}}" mode="aspectFit"/></view></picker><text wx:if="{{aO}}" class="field-error data-v-dcfd67c8">{{aP}}</text></view><view wx:if="{{aQ}}" class="field-block last-field data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">手机号</text><view class="{{['input-shell', 'data-v-dcfd67c8', aT && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="input-placeholder" bindinput="{{aR}}" value="{{aS}}"/></view><text wx:if="{{aU}}" class="field-error data-v-dcfd67c8">{{aV}}</text></view></view><view wx:if="{{aW}}" class="form-card data-v-dcfd67c8"><text class="section-title data-v-dcfd67c8">管理员信息</text><view class="field-block data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">管理员姓名</text><view class="{{['input-shell', 'data-v-dcfd67c8', aZ && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" maxlength="30" placeholder="请输入管理员姓名" placeholder-class="input-placeholder" bindinput="{{aX}}" value="{{aY}}"/></view><text wx:if="{{ba}}" class="field-error data-v-dcfd67c8">{{bb}}</text></view><view class="field-block last-field data-v-dcfd67c8"><text class="field-label data-v-dcfd67c8">管理员手机号</text><view class="{{['input-shell', 'data-v-dcfd67c8', be && 'field-invalid']}}"><input class="text-input data-v-dcfd67c8" type="number" maxlength="11" placeholder="请输入管理员手机号" placeholder-class="input-placeholder" bindinput="{{bc}}" value="{{bd}}"/></view><text wx:if="{{bf}}" class="field-error data-v-dcfd67c8">{{bg}}</text></view></view><view class="{{['submit-button', 'data-v-dcfd67c8', bi && 'disabled']}}" bindtap="{{bj}}"><text class="data-v-dcfd67c8">{{bh}}</text></view><view class="safe-space data-v-dcfd67c8"></view></scroll-view><guide-dialog wx:if="{{bm}}" class="data-v-dcfd67c8" bindcancel="{{bk}}" bindconfirm="{{bl}}" u-i="dcfd67c8-1" bind:__l="__l" u-p="{{bm}}"/></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxss
new file mode 100644
index 0000000..14febd4
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/onboarding/index.wxss
@@ -0,0 +1 @@
+.page.data-v-dcfd67c8{display:flex;flex-direction:column;height:100vh;min-height:0;background:#f6f8fb;padding:0;padding-bottom:env(safe-area-inset-bottom);overflow:hidden;animation:pageSlideIn-dcfd67c8 .26s cubic-bezier(.22,1,.36,1) both}.page.leaving.data-v-dcfd67c8{animation:pageSlideOut-dcfd67c8 .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-dcfd67c8{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-dcfd67c8{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.form-scroll.data-v-dcfd67c8{flex:1;height:0;min-height:0;padding:0 28rpx;box-sizing:border-box}.form-card.data-v-dcfd67c8{margin-top:26rpx;padding:30rpx;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff;box-sizing:border-box}.subject-card.data-v-dcfd67c8{margin-top:24rpx;padding-top:20rpx;padding-bottom:20rpx}.merchant-type-row.data-v-dcfd67c8{display:flex;min-height:92rpx;flex-direction:row;align-items:center}.type-copy.data-v-dcfd67c8{display:flex;flex:1;flex-direction:column}.inline-label.data-v-dcfd67c8{margin-bottom:0}.type-control.data-v-dcfd67c8{display:flex;width:190rpx;height:72rpx;padding:0 18rpx 0 22rpx;flex-direction:row;align-items:center;justify-content:space-between;border:1rpx solid #e1e6ed;border-radius:12rpx;color:#1d2939;font-size:27rpx;background:#f7f9fc;box-sizing:border-box}.section-title.data-v-dcfd67c8{display:block;margin-bottom:28rpx;color:#1f3b5b;font-size:31rpx;font-weight:700}.field-block.data-v-dcfd67c8{display:flex;margin-bottom:26rpx;flex-direction:column}.last-field.data-v-dcfd67c8{margin-bottom:0}.field-label.data-v-dcfd67c8{display:block;margin-bottom:13rpx;color:#344054;font-size:27rpx;font-weight:600}.input-shell.data-v-dcfd67c8,.select-shell.data-v-dcfd67c8{display:flex;width:100%;height:82rpx;padding:0 22rpx;flex-direction:row;align-items:center;border:1rpx solid #cfd5dd;border-radius:12rpx;background:#fff;box-sizing:border-box}.text-input.data-v-dcfd67c8{flex:1;height:80rpx;color:#1d2939;font-size:27rpx}.input-placeholder.data-v-dcfd67c8,.select-placeholder.data-v-dcfd67c8{color:#a8b1be;font-size:26rpx}.select-shell.data-v-dcfd67c8{justify-content:space-between;color:#1d2939;font-size:27rpx}.readonly-shell.data-v-dcfd67c8{color:#5a6472;background:#f0f2f5;border-color:#f0f2f5}.date-picker.data-v-dcfd67c8,.region-picker.data-v-dcfd67c8{width:100%}.down-icon.data-v-dcfd67c8{width:30rpx;height:30rpx;margin-left:14rpx;flex:none}.ellipsis-text.data-v-dcfd67c8{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-invalid.data-v-dcfd67c8{border-color:#f0746d;background:#fff9f8}.field-error.data-v-dcfd67c8{margin-top:9rpx;color:#e95b55;font-size:23rpx}.submit-button.data-v-dcfd67c8{display:flex;height:88rpx;margin-top:34rpx;justify-content:center;align-items:center;border-radius:12rpx;color:#fff;font-size:29rpx;font-weight:700;background:#4d8fe8}.submit-button.disabled.data-v-dcfd67c8{opacity:.58}.safe-space.data-v-dcfd67c8{height:calc(70rpx + env(safe-area-inset-bottom))}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.js
new file mode 100644
index 0000000..d1d4b2b
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.js
@@ -0,0 +1 @@
+"use strict";const e=require("../../common/vendor.js"),t={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js",GuideDialog:()=>"../../components/guide-dialog/guide-dialog.js"},data:()=>({amount:"",availableAmount:0,cards:[],selectedId:"",error:"",submitting:!1,confirmVisible:!1,isLeaving:!1}),computed:{availableText(){return Number(this.availableAmount||0).toFixed(2)},selected(){return this.cards.find(e=>e.id===this.selectedId)||null},confirmContent(){const e=this.selected&&this.selected.bankName||"已绑定银行卡";return"确认提现 ¥"+Number(this.amount||0).toFixed(2)+" 至"+e+"吗?"}},onShow(){this.loadSummary()},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{e.backOrHome("/pages/withdrawal/index")},280))},clearError(){this.error=""},chooseBank(){e.routeTo("/pages/bank-card-select/index")},loadSummary(){e.getWithdrawalSummary().then(e=>{this.availableAmount=Number(e&&e.availableAmount||0)}).catch(e=>{this.error=e.message||"可提金额加载失败,请稍后重试"}),e.getBankCards().then(e=>{this.cards=e||[]}).catch(e=>{this.error=e.message||"银行卡加载失败,请稍后重试"});const t=e.consumeSelectedBankId();t&&(this.selectedId=t)},submit(){if(this.submitting)return;const e=Number(this.amount),t=Math.round(100*e),i=Math.round(100*Number(this.availableAmount||0));!Number.isFinite(e)||t<=0||t>i?this.error="请输入不超过当前可提金额的提现金额":this.selected?(this.error="",this.confirmVisible=!0):this.error="请选择审核通过的银行卡"},confirmSubmit(){this.submitting||(this.submitting=!0,e.createWithdrawal(Number(Number(this.amount).toFixed(2)),this.selected).then(t=>{this.submitting=!1,e.index.redirectTo({url:"/pages/withdrawal-success/index?withdrawalNo="+encodeURIComponent(t&&t.withdrawalNo||"")})}).catch(e=>{this.submitting=!1,this.error=e.message||"提现提交失败,请重试"}))}}};if(!Array){(e.resolveComponent("guide-nav-bar")+e.resolveComponent("guide-dialog"))()}Math||((()=>"../../components/guide-nav-bar/guide-nav-bar.js")+(()=>"../../components/guide-dialog/guide-dialog.js"))();const i=e._export_sfc(t,[["render",function(t,i,r,a,s,o){return e.e({a:e.o(o.goBack,"bd"),b:e.p({title:"导游提现"}),c:e.t(o.availableText),d:e.o([e=>s.amount=e.detail.value,(...e)=>o.clearError&&o.clearError(...e)],"fd"),e:s.amount,f:s.error?1:"",g:s.error},s.error?{h:e.t(s.error)}:{},{i:o.selected},o.selected?{j:e.t(o.selected.bankName),k:e.t(o.selected.cardMasked)}:{},{l:e.t(o.selected?"更换":"选择"),m:e.o((...e)=>o.chooseBank&&o.chooseBank(...e),"e8"),n:e.t(s.submitting?"提交中":"发起提现"),o:s.submitting?1:"",p:e.o((...e)=>o.submit&&o.submit(...e),"c6"),q:e.o(e=>s.confirmVisible=!1,"fb"),r:e.o(o.confirmSubmit,"9a"),s:e.p({visible:s.confirmVisible,title:"确认提现",content:o.confirmContent,"confirm-text":"确认提现"}),t:s.isLeaving?1:""})}],["__scopeId","data-v-7abd866f"]]);wx.createPage(i);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.json
new file mode 100644
index 0000000..59b99bc
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.json
@@ -0,0 +1,10 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "导游提现",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar",
+ "guide-dialog": "../../components/guide-dialog/guide-dialog"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxml
new file mode 100644
index 0000000..f9df2c3
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxml
@@ -0,0 +1 @@
+<view class="{{['detail-page', 'data-v-7abd866f', t && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-7abd866f" bindback="{{a}}" u-i="7abd866f-0" bind:__l="__l" u-p="{{b}}"/><scroll-view class="form-scroll data-v-7abd866f" scroll-y><view class="form-card data-v-7abd866f"><view class="available-row data-v-7abd866f"><text class="available-label data-v-7abd866f">当前可提</text><text class="available-value data-v-7abd866f">¥{{c}}</text></view><view class="field-block data-v-7abd866f"><view class="field-row data-v-7abd866f"><text class="field-label data-v-7abd866f">提现金额</text><view class="{{['amount-shell', 'data-v-7abd866f', f && 'invalid']}}"><text class="amount-prefix data-v-7abd866f">¥</text><input class="amount-input data-v-7abd866f" type="digit" maxlength="10" placeholder="请输入" placeholder-class="input-placeholder" bindinput="{{d}}" value="{{e}}"/></view></view><text wx:if="{{g}}" class="field-error data-v-7abd866f">{{h}}</text></view><view class="field-divider data-v-7abd866f"></view><view class="field-row bank-row data-v-7abd866f"><text class="field-label data-v-7abd866f">提现银行卡</text><view class="bank-value data-v-7abd866f"><view wx:if="{{i}}" class="selected-bank data-v-7abd866f"><text class="bank-name data-v-7abd866f">{{j}}</text><text class="card-number data-v-7abd866f">{{k}}</text></view><text wx:else class="bank-placeholder data-v-7abd866f">请选择银行卡</text></view><view class="select-bank-button data-v-7abd866f" bindtap="{{m}}"><text class="data-v-7abd866f">{{l}}</text></view></view></view><view class="{{['submit-button', 'data-v-7abd866f', o && 'disabled']}}" bindtap="{{p}}"><text class="data-v-7abd866f">{{n}}</text></view></scroll-view><guide-dialog wx:if="{{s}}" class="data-v-7abd866f" bindcancel="{{q}}" bindconfirm="{{r}}" u-i="7abd866f-1" bind:__l="__l" u-p="{{s}}"/></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxss
new file mode 100644
index 0000000..a88f98a
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-detail/index.wxss
@@ -0,0 +1 @@
+.detail-page.data-v-7abd866f{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-7abd866f .26s cubic-bezier(.22,1,.36,1) both}.detail-page.leaving.data-v-7abd866f{animation:pageSlideOut-7abd866f .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-7abd866f{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-7abd866f{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.form-scroll.data-v-7abd866f{flex:1;min-height:0;padding:28rpx 28rpx 40rpx;box-sizing:border-box}.form-card.data-v-7abd866f{padding:10rpx 28rpx 14rpx;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff}.available-row.data-v-7abd866f{display:flex;align-items:center;height:72rpx}.available-label.data-v-7abd866f{color:#667085;font-size:25rpx}.available-value.data-v-7abd866f{margin-left:12rpx;color:#1f3b5b;font-size:27rpx;font-weight:700}.field-row.data-v-7abd866f{display:flex;min-height:104rpx;align-items:center}.field-label.data-v-7abd866f{width:178rpx;flex:none;color:#344054;font-size:27rpx}.amount-shell.data-v-7abd866f{display:flex;height:70rpx;padding:0 18rpx;flex:1;align-items:center;border:1rpx solid #cfd5dd;border-radius:12rpx;box-sizing:border-box}.amount-shell.invalid.data-v-7abd866f{border-color:#f0746d;background:#fff9f8}.amount-prefix.data-v-7abd866f{color:#8b94a2;font-size:27rpx}.amount-input.data-v-7abd866f{height:68rpx;margin-left:8rpx;flex:1;color:#1d2939;font-size:27rpx}.input-placeholder.data-v-7abd866f{color:#b5bdc9;font-size:25rpx}.field-error.data-v-7abd866f{margin:0 0 14rpx 178rpx;color:#e95b55;font-size:22rpx}.field-divider.data-v-7abd866f{height:1rpx;margin-left:178rpx;background:#edf0f4}.bank-value.data-v-7abd866f{min-width:0;flex:1}.selected-bank.data-v-7abd866f{display:flex;flex-direction:column}.bank-name.data-v-7abd866f{color:#1d2939;font-size:25rpx}.card-number.data-v-7abd866f{margin-top:4rpx;color:#8a93a1;font-size:22rpx}.bank-placeholder.data-v-7abd866f{color:#b5bdc9;font-size:25rpx}.select-bank-button.data-v-7abd866f{display:flex;width:84rpx;height:52rpx;margin-left:14rpx;flex:none;align-items:center;justify-content:center;border:1rpx solid #58a5f8;border-radius:12rpx;color:#318cf0;background:#fff;font-size:23rpx}.submit-button.data-v-7abd866f{display:flex;width:100%;height:88rpx;margin-top:34rpx;align-items:center;justify-content:center;border-radius:12rpx;color:#fff;background:#4d8fe8;font-size:30rpx;font-weight:700}.submit-button.disabled.data-v-7abd866f{opacity:.58}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.js
new file mode 100644
index 0000000..9d50631
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.js
@@ -0,0 +1 @@
+"use strict";const a=require("../../common/vendor.js"),r=require("../../common/assets.js"),t={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js"},data:()=>({withdrawalNo:""}),onLoad(a){this.withdrawalNo=a&&a.withdrawalNo?String(a.withdrawalNo):""},methods:{returnToWithdrawal(){a.index.navigateBack({fail(){a.backOrHome("/pages/withdrawal/index")}})}}};if(!Array){a.resolveComponent("guide-nav-bar")()}Math;const e=a._export_sfc(t,[["render",function(t,e,o,i,d,n){return a.e({a:a.o(n.returnToWithdrawal,"fc"),b:a.p({title:"导游提现"}),c:r._imports_1,d:d.withdrawalNo},d.withdrawalNo?{e:a.t(d.withdrawalNo)}:{},{f:a.o((...a)=>n.returnToWithdrawal&&n.returnToWithdrawal(...a),"2b")})}],["__scopeId","data-v-e9d8ca8f"]]);wx.createPage(e);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.json
new file mode 100644
index 0000000..bea7622
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "导游提现",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxml
new file mode 100644
index 0000000..52d6814
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxml
@@ -0,0 +1 @@
+<view class="success-page data-v-e9d8ca8f"><guide-nav-bar wx:if="{{b}}" class="data-v-e9d8ca8f" bindback="{{a}}" u-i="e9d8ca8f-0" bind:__l="__l" u-p="{{b}}"/><view class="result-content data-v-e9d8ca8f"><view class="success-icon-shell data-v-e9d8ca8f"><image class="success-icon data-v-e9d8ca8f" src="{{c}}" mode="aspectFit"/></view><text class="result-title data-v-e9d8ca8f">提交成功,等待审核</text><text wx:if="{{d}}" class="withdrawal-no data-v-e9d8ca8f">提现单号:{{e}}</text><text class="result-tip data-v-e9d8ca8f">提现申请已提交,平台审核通过后将发起到账处理,请留意提现记录状态。</text><view class="return-button data-v-e9d8ca8f" bindtap="{{f}}"><text class="data-v-e9d8ca8f">返回</text></view></view></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxss
new file mode 100644
index 0000000..b3b5477
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal-success/index.wxss
@@ -0,0 +1 @@
+.success-page.data-v-e9d8ca8f{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding-bottom:env(safe-area-inset-bottom)}.result-content.data-v-e9d8ca8f{display:flex;flex:1;padding:96rpx 48rpx 0;flex-direction:column;align-items:center;box-sizing:border-box}.success-icon-shell.data-v-e9d8ca8f{display:flex;width:140rpx;height:140rpx;align-items:center;justify-content:center;border-radius:70rpx;background:#eef6ff}.success-icon.data-v-e9d8ca8f{width:72rpx;height:72rpx}.result-title.data-v-e9d8ca8f{margin-top:42rpx;color:#1f3b5b;font-size:30rpx;font-weight:700}.withdrawal-no.data-v-e9d8ca8f{margin-top:22rpx;color:#667085;font-size:25rpx}.result-tip.data-v-e9d8ca8f{margin-top:14rpx;color:#667085;font-size:23rpx;text-align:center;line-height:34rpx}.return-button.data-v-e9d8ca8f{display:flex;width:100%;height:88rpx;margin-top:86rpx;align-items:center;justify-content:center;border-radius:12rpx;color:#fff;background:#4d8fe8;font-size:29rpx;font-weight:700}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.js b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.js
new file mode 100644
index 0000000..d9675d1
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.js
@@ -0,0 +1 @@
+"use strict";const t=require("../../common/vendor.js"),a={components:{GuideNavBar:()=>"../../components/guide-nav-bar/guide-nav-bar.js"},data:()=>({loading:!1,availableAmount:0,records:[],total:0,isLeaving:!1}),computed:{availableText(){return this.formatMoney(this.availableAmount)}},onShow(){this.loadData()},methods:{goBack(){this.isLeaving||(this.isLeaving=!0,setTimeout(()=>{t.backOrHome("/pages/earnings/index")},280))},startWithdrawal(){t.getOnboardingStatus().then(a=>{const e=a&&a.status||"unsubmitted";"approved"!==e?"pending"!==e&&"rejected"!==e?t.routeTo("/pages/onboarding/index"):t.routeTo("/pages/onboarding-pending/index"):t.routeTo("/pages/withdrawal-detail/index")}).catch(a=>{t.index.showToast({title:a&&a.message||"进件状态查询失败",icon:"none"})})},loadData(){this.loading=!0,Promise.all([t.getWithdrawalSummary(),t.getWithdrawals()]).then(([t,a])=>{this.availableAmount=Number(t&&t.availableAmount||0),this.records=a&&a.records||[],this.total=Number(a&&a.total||0)}).catch(a=>{t.index.showToast({title:a.message||"提现记录加载失败",icon:"none"})}).finally(()=>{this.loading=!1})},formatMoney:(t=0)=>Number(t||0).toFixed(2),formatTime:t=>t?String(t).replace("T"," ").slice(0,19):"",displayTime(t){return"APPROVED"===t.status&&t.processedAt?"到账 "+this.formatTime(t.processedAt):this.formatTime(t.appliedAt)},statusText:t=>"APPROVED"===t?"已到账":"REJECTED"===t?"已驳回":"PROCESSING"===t?"到账处理中":"待审核",statusClass:t=>"APPROVED"===t?"paid":"REJECTED"===t?"rejected":"pending"}};if(!Array){t.resolveComponent("guide-nav-bar")()}Math;const e=t._export_sfc(a,[["render",function(a,e,s,o,i,n){return t.e({a:t.o(n.goBack,"c5"),b:t.p({title:"导游提现"}),c:t.t(n.availableText),d:t.o((...t)=>n.startWithdrawal&&n.startWithdrawal(...t),"1e"),e:t.t(i.total),f:i.loading},(i.loading||i.records.length,{}),{g:!i.records.length,h:t.f(i.records,(a,e,s)=>t.e({a:t.t(n.formatMoney(a.amount)),b:t.t(n.displayTime(a)),c:t.t(a.cardMasked||""),d:t.t(n.statusText(a.status)),e:t.n("status "+n.statusClass(a.status)),f:a.reason},a.reason?{g:t.t(a.reason)}:{},{h:a.id})),i:i.isLeaving?1:""})}],["__scopeId","data-v-88232326"]]);wx.createPage(e);
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.json b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.json
new file mode 100644
index 0000000..bea7622
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "导游提现",
+ "navigationBarBackgroundColor": "#4d8fe8",
+ "navigationBarTextStyle": "white",
+ "usingComponents": {
+ "guide-nav-bar": "../../components/guide-nav-bar/guide-nav-bar"
+ }
+}
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxml b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxml
new file mode 100644
index 0000000..a2dec12
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxml
@@ -0,0 +1 @@
+<view class="{{['withdrawal-page', 'data-v-88232326', i && 'leaving']}}"><guide-nav-bar wx:if="{{b}}" class="data-v-88232326" bindback="{{a}}" u-i="88232326-0" bind:__l="__l" u-p="{{b}}"/><view class="balance-card data-v-88232326"><text class="balance-label data-v-88232326">当前可提</text><text class="balance-amount data-v-88232326">¥{{c}}</text><view class="withdraw-button data-v-88232326" bindtap="{{d}}">提现</view></view><view class="heading data-v-88232326"><text class="data-v-88232326">提现记录</text><text class="data-v-88232326">共 {{e}} 条</text></view><scroll-view class="record-list data-v-88232326" scroll-y><view wx:if="{{f}}" class="state-text data-v-88232326">正在加载</view><view wx:elif="{{g}}" class="state-text data-v-88232326">暂无提现记录</view><view wx:for="{{h}}" wx:for-item="item" wx:key="h" class="record data-v-88232326"><view class="record-top data-v-88232326"><text class="data-v-88232326">提现</text><text class="record-money data-v-88232326">¥{{item.a}}</text></view><view class="record-bottom data-v-88232326"><text class="data-v-88232326">{{item.b}} {{item.c}}</text><text class="{{['data-v-88232326', item.e]}}">{{item.d}}</text></view><text wx:if="{{item.f}}" class="reason data-v-88232326">{{item.g}}</text></view></scroll-view></view>
\ No newline at end of file
diff --git a/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxss b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxss
new file mode 100644
index 0000000..329afab
--- /dev/null
+++ b/mingdaojingjiang_guide_app/dist/build/mp-weixin/pages/withdrawal/index.wxss
@@ -0,0 +1 @@
+.withdrawal-page.data-v-88232326{display:flex;flex-direction:column;min-height:100vh;background:#f6f8fb;padding-bottom:env(safe-area-inset-bottom);animation:pageSlideIn-88232326 .26s cubic-bezier(.22,1,.36,1) both}.withdrawal-page.leaving.data-v-88232326{animation:pageSlideOut-88232326 .28s cubic-bezier(.4,0,1,1) both}@keyframes pageSlideIn-88232326{0%{opacity:.35;transform:translate(72rpx)}to{opacity:1;transform:translate(0)}}@keyframes pageSlideOut-88232326{0%{opacity:1;transform:translate(0)}to{opacity:.2;transform:translate(260rpx)}}.balance-card.data-v-88232326{margin:28rpx 28rpx 0;padding:36rpx 32rpx 32rpx;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff;text-align:center}.balance-label.data-v-88232326{color:#667085;font-size:26rpx}.balance-amount.data-v-88232326{display:block;margin:16rpx 0 28rpx;color:#1f3b5b;font-size:56rpx;font-weight:800;line-height:1}.withdraw-button.data-v-88232326{display:flex;align-items:center;justify-content:center;height:88rpx;border-radius:12rpx;background:#4d8fe8;color:#fff;font-size:30rpx;font-weight:700}.heading.data-v-88232326{display:flex;justify-content:space-between;padding:36rpx 36rpx 18rpx;color:#1f3b5b;font-size:30rpx;font-weight:700}.heading text.data-v-88232326:last-child{color:#a0a9b7;font-size:24rpx;font-weight:400}.record-list.data-v-88232326{flex:1;min-height:0;padding:0 28rpx 24rpx;box-sizing:border-box}.record.data-v-88232326{padding:26rpx 28rpx;margin-bottom:16rpx;border:1rpx solid #edf0f4;border-radius:12rpx;background:#fff}.record-top.data-v-88232326,.record-bottom.data-v-88232326{display:flex;justify-content:space-between;align-items:center}.record-top.data-v-88232326{color:#1f2937;font-size:28rpx;font-weight:700}.record-money.data-v-88232326{font-weight:800}.record-bottom.data-v-88232326{margin-top:16rpx;color:#667085;font-size:23rpx}.status.data-v-88232326{font-weight:700}.pending.data-v-88232326{color:#e58b28}.paid.data-v-88232326{color:#249765}.rejected.data-v-88232326,.reason.data-v-88232326{color:#e95b5b}.reason.data-v-88232326{display:block;margin-top:8rpx;font-size:22rpx}.state-text.data-v-88232326{padding-top:120rpx;color:#8993a1;text-align:center;font-size:28rpx}
\ No newline at end of file
--
Gitblit v1.7.1