☰
本地可运行的P2P金融系统:MySQL+ES+Spring Boot实战
2026/11/5 2:17:30 网站建设 项目流程

简介:这是一套完整的Java微服务架构P2P金融平台源码,面向中高级Java开发者与金融科技系统学习者,聚焦高并发、强安全、多组件协同的金融级系统实践。资源涵盖用户借贷管理、资金存管、还款清算等核心业务模块,深度整合MySQL持久化、Elasticsearch实时检索、Apollo分布式配置及Spring生态(含Security鉴权、AOP日志等),并体现微服务拆分、异步消息解耦与生产级监控思路。压缩包共1153个文件,以215个Java业务逻辑类、409个XML配置/映射文件、228个编译后Class字节码及18个YML/Properties环境配置为主,辅以Log日志、Dockerfile容器化支持和HTML/PNG文档素材,整体仅1.57MB,结构紧凑、模块边界清晰。已有3084人学习下载,读者可直接运行调试,掌握P2P平台从账户体系、标的管理(Project/Tender)、资金流水(DepositoryRecord)到风控校验(SecurityConfig)、时间工具(DateUtil)等全链路实现细节,并复用其分层设计与安全实践模式。

1. 一个跑在本地就能模拟真实P2P资金撮合与风控闭环的Java工程:MySQL存核心账务、ES做实时标的检索、Spring Boot搭骨架,适合想补全金融系统实战链路的中阶开发者

你有没有试过——在本地IDE里点开一个项目,mvn clean install之后,java -jar target/xxx.jar,不到两分钟,浏览器打开http://localhost:8080/admin,就能看到「待审核借款」列表、「用户资产看板」、「标的搜索页」,甚至还能手动触发一笔「自动投标」,看着资金从出借人账户扣减、进入标的池、再生成还款计划表?这不是Demo,这是万信金融P2P项目的源码包。它不追求高并发压测或分布式事务黑科技,而是把「资金流+信息流+风控流」三股线拧成一股可调试、可打断点、可改SQL、可换ES查询DSL的完整业务流。它用最朴素的Spring Boot + MyBatis + MySQL组合落地账务一致性,用ES替代数据库LIKE模糊查标的性能黑洞,所有接口都带Swagger文档,连还款计划生成逻辑都拆成了RepaymentPlanService.calculate()可单测方法。如果你正卡在「学了Spring Boot但写不出有资金状态机的系统」「知道ES但没在真实借贷场景里调过bool+should+range嵌套查询」「能写CRUD却不敢碰还款计算中的闰年处理和360天计息规则」——这个项目就是为你留的调试沙盒。


2. 项目结构与技术栈解剖:三层分包+双数据源+ES索引映射设计

2.1 目录结构即业务脉络:从loan到repayment的模块命名直指核心域

项目采用标准Maven多模块结构,但模块划分不是按技术层(如xxx-web,xxx-service),而是严格按P2P核心业务域切分:

wanxin-p2p/ ├── wanxin-p2p-api/ # OpenAPI定义:Swagger注解+DTO+Feign Client ├── wanxin-p2p-common/ # 公共工具:金额精度处理(BigDecimalUtils)、日期工具(DateUtils)、异常码枚举(ResultCode) ├── wanxin-p2p-finance/ # 资金核心:账户管理、充值/提现、投标/流标、还款计划生成、资金划转(含事务边界标注) ├── wanxin-p2p-loan/ # 借款核心:借款人认证、标的创建、审核流程(状态机驱动)、标的搜索(ES集成) ├── wanxin-p2p-user/ # 用户中心:实名认证、银行卡绑定、风险测评、用户等级 ├── wanxin-p2p-gateway/ # 网关:JWT鉴权、限流(Sentinel)、日志埋点 └── wanxin-p2p-admin/ # 后台管理:基于Vue的前端+后端AdminController(独立profile激活)

提示:wanxin-p2p-finance和wanxin-p2p-loan是绝对核心。前者管“钱怎么动”,后者管“钱往哪投”。所有资金操作(如InvestService.invest())最终都会调用FinanceAccountService更新账户余额,并通过TransactionLogMapper.insert()落库记账;而所有标的搜索请求(如/loan/list?keyword=小微企业)则绕过MySQL,直连ES的loan_index索引。

2.2 技术选型背后的务实决策:为什么MySQL不扛搜索、ES不存账务

组件版本承担角色关键设计理由
MySQL 5.7mysql-connector-java:8.0.28主库:用户信息、账户余额、交易流水、审核记录强一致性要求:每笔投标必须保证“用户余额够+标的未满标+状态可投”三条件原子性;使用SELECT ... FOR UPDATE锁行防超投
Elasticsearch 7.10spring-boot-starter-data-elasticsearch:2.4.13搜索库:标的详情(title/description/rate/term)、借款人资质摘要、标签(如“小微企业”“信用贷”)MySQL LIKE查询在10万+标的下响应超2s;ES通过text字段分词+keyword字段精确匹配,支持multi_match跨字段搜,响应稳定在150ms内
Spring Boot 2.4.13—应用框架选择2.4.x而非3.x,因项目依赖的mybatis-spring-boot-starter:2.1.4与Spring Boot 3.x的Jakarta EE迁移存在兼容问题,避免踩坑
MyBatis Plus 3.4.3—ORM层@TableName("finance_account")显式绑定表名,避免动态表名导致的SQL注入风险;LambdaQueryWrapper写法杜绝硬编码字段名

注意:项目没有用ShardingSphere分库分表,也没有引入RocketMQ做异步解耦。所有资金划转(如还款成功后向出借人打款)都是同步调用FinanceTransferService.transfer()完成。这恰恰是它的价值——让你看清资金在单体应用内如何被精准控制,而不是一上来就被分布式事务绕晕。

2.3 双数据源配置:MySQL主库与ES搜索库的YAML声明式隔离

application.yml中明确分离数据源,且ES配置指向独立集群(非localhost单节点):

# application.yml spring: datasource: url: jdbc:mysql://192.168.10.100:3306/wanxin_p2p?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: p2p_user password: p2p_pass_2023 driver-class-name: com.mysql.cj.jdbc.Driver elasticsearch: uris: http://192.168.10.101:9200,http://192.168.10.102:9200 # ES集群地址,非单机 username: elastic password: changeme

对应Java配置类ElasticsearchConfig.java中,关键点在于禁用默认的RestHighLevelClient自动装配,手动构建带连接池的客户端:

@Configuration public class ElasticsearchConfig { @Value("${spring.elasticsearch.uris}") private String[] uris; @Value("${spring.elasticsearch.username}") private String username; @Value("${spring.elasticsearch.password}") private String password; @Bean @Primary public RestHighLevelClient restHighLevelClient() { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); RestClientBuilder builder = RestClient.builder( Arrays.stream(uris).map(HttpHost::create).toArray(HttpHost[]::new)) .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) .setMaxConnTotal(100) // 连接池总大小 .setMaxConnPerRoute(20)); // 每路由最大连接数 return new RestHighLevelClient(builder); } }

这段代码决定了ES的稳定性:setMaxConnTotal(100)防止高并发搜索时连接耗尽;setDefaultCredentialsProvider确保集群启用了X-Pack安全插件也能连上。如果你本地ES没开安全认证,只需删掉credentialsProvider相关代码即可。

2.4 ES索引设计:loan_index的mapping如何支撑复杂标的筛选

项目在启动时自动创建loan_index,其mapping定义直接暴露了业务筛选逻辑:

PUT /loan_index { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "analysis": { "analyzer": { "ik_max_word": { "type": "custom", "tokenizer": "ik_max_word" } } } }, "mappings": { "properties": { "id": { "type": "long" }, "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "description": { "type": "text", "analyzer": "ik_max_word" }, "rate": { "type": "double", "index": true }, // 年化利率,需范围查询 "term": { "type": "integer", "index": true }, // 借款期限(月),需范围查询 "amount": { "type": "double", "index": true }, // 借款金额,需范围查询 "status": { "type": "keyword" }, // 标的状态:APPROVED(已审核)、FULLY_FUNDED(已满标) "borrowerLevel": { "type": "keyword" }, // 借款人等级:A/B/C "tags": { "type": "keyword" } // 标签数组:["小微企业","信用贷"] } } }

关键设计点:

  • title和description用ik_max_word分词器,支持“小微企业贷款”被拆为[小微企业, 小微, 企业, 贷款],提升召回率;
  • rate/term/amount设为double/integer并开启index:true,支撑range查询(如rate >= 10 AND term <= 12);
  • status和borrowerLevel用keyword类型,保证精确匹配(避免status被分词成s,t,a,t,u,s);
  • tags是keyword数组,支撑terms查询(如tags IN ["小微企业","信用贷"])。

提示:项目未使用IK分词器的远程词典热更新功能,所有词典文件(main.dic,quantifier.dic)均放在resources/ik目录下,启动时自动加载。若你要加行业词,直接往main.dic追加一行即可,无需重启ES。


3. 核心业务链路代码逐行解析:从用户投标到生成还款计划

3.1 投标入口:InvestController.invest()的事务边界与状态校验

wanxin-p2p-loan模块下的InvestController.java是资金流入的起点。它不直接操作数据库,而是调用InvestService,并用@Transactional声明式事务包裹整个投标流程:

@RestController @RequestMapping("/invest") public class InvestController { @Autowired private InvestService investService; @PostMapping("/doInvest") @Transactional(rollbackFor = Exception.class) // 关键:事务回滚针对所有Exception public Result invest(@RequestBody InvestRequest request, @RequestHeader("token") String token) { // 1. 解析用户ID(从JWT token中) Long userId = JwtUtil.getUserId(token); // 2. 执行投标主逻辑 Boolean success = investService.doInvest(userId, request.getLoanId(), request.getAmount()); return success ? Result.success("投标成功") : Result.fail("投标失败"); } }

这段代码的玄学在于:@Transactional必须作用于public方法,且调用必须走Spring代理。如果InvestController里直接new InvestService().doInvest(),事务会失效!这也是新手常翻车的点。

3.2 投标主逻辑:InvestService.doInvest()的四重校验与资金冻结

InvestService.java中的doInvest()是真正的业务心脏,它串联了账户检查、标的检查、资金冻结、记录生成四步:

@Service public class InvestService { @Autowired private FinanceAccountService financeAccountService; // 账户服务 @Autowired private LoanInfoMapper loanInfoMapper; // 标的信息Mapper @Autowired private InvestRecordMapper investRecordMapper; // 投标记录Mapper @Transactional(rollbackFor = Exception.class) public Boolean doInvest(Long userId, Long loanId, BigDecimal amount) { // Step 1: 检查用户账户余额是否充足(SELECT ... FOR UPDATE) FinanceAccount account = financeAccountService.getAccountByUserId(userId); if (account.getAvailableBalance().compareTo(amount) < 0) { throw new BusinessException("可用余额不足"); } // Step 2: 检查标的是否可投(状态=APPROVED且未满标) LoanInfo loan = loanInfoMapper.selectById(loanId); if (!"APPROVED".equals(loan.getStatus()) || loan.getRemainAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new BusinessException("标的不可投"); } // Step 3: 冻结用户资金(更新账户可用余额,冻结余额增加) financeAccountService.freezeFunds(userId, amount); // 此方法内执行UPDATE // Step 4: 创建投标记录(状态=INVESTING) InvestRecord record = new InvestRecord(); record.setUserId(userId); record.setLoanId(loanId); record.setInvestAmount(amount); record.setStatus("INVESTING"); // 待放款状态 investRecordMapper.insert(record); return true; } }

关键参数说明:

  • financeAccountService.freezeFunds()内部执行SQL:
    UPDATE finance_account SET available_balance = available_balance - #{amount}, frozen_balance = frozen_balance + #{amount} WHERE user_id = #{userId}
    这条SQL必须用SELECT ... FOR UPDATE先锁住该用户账户行,否则并发投标会导致超投。
  • loan.getRemainAmount()是MySQL中计算列:(loan_amount - COALESCE(invested_amount, 0)),确保实时性。
  • InvestRecord.status设为INVESTING而非SUCCESS,因为资金真正划转(从冻结到标的池)需等风控审核通过后由LoanAuditService触发。

3.3 还款计划生成:RepaymentPlanService.calculate()的360天计息与闰年适配

wanxin-p2p-finance模块的RepaymentPlanService.java实现了P2P最易出错的还款计算。它不调用外部服务,纯Java实现,且显式处理了金融级精度问题:

@Service public class RepaymentPlanService { /** * 生成等额本息还款计划 * @param loanAmount 借款本金(元) * @param annualRate 年化利率(%),如12.0 * @param term 期限(月) * @param startDate 起息日(格式:yyyy-MM-dd) * @return 还款计划列表(按期次升序) */ public List<RepaymentPlan> calculate(BigDecimal loanAmount, BigDecimal annualRate, Integer term, String startDate) { List<RepaymentPlan> plans = new ArrayList<>(); BigDecimal monthlyRate = annualRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP) .divide(new BigDecimal("12"), 6, RoundingMode.HALF_UP); BigDecimal monthPayment = calculateMonthPayment(loanAmount, monthlyRate, term); LocalDate currentDate = LocalDate.parse(startDate); BigDecimal remainingPrincipal = loanAmount; for (int i = 1; i <= term; i++) { // 每期利息 = 剩余本金 × 月利率(360天计息法:每月固定30天) BigDecimal interest = remainingPrincipal.multiply(monthlyRate) .setScale(2, RoundingMode.HALF_UP); // 每期本金 = 月供 - 利息 BigDecimal principal = monthPayment.subtract(interest) .setScale(2, RoundingMode.HALF_UP); // 防止最后一期因四舍五入导致本金为负 if (i == term) { principal = remainingPrincipal; interest = monthPayment.subtract(principal); } // 下期还款日:当前日+30天(360天法,不考虑实际日历) LocalDate repayDate = currentDate.plusDays(30); currentDate = repayDate; RepaymentPlan plan = new RepaymentPlan(); plan.setTerm(i); plan.setRepayDate(repayDate.toString()); plan.setPrincipal(principal); plan.setInterest(interest); plan.setTotalAmount(monthPayment); plan.setRemainingPrincipal(remainingPrincipal.subtract(principal).setScale(2, RoundingMode.HALF_UP)); plans.add(plan); remainingPrincipal = remainingPrincipal.subtract(principal); } return plans; } private BigDecimal calculateMonthPayment(BigDecimal loanAmount, BigDecimal monthlyRate, Integer term) { // 等额本息公式:月供 = 本金 × 月利率 × (1+月利率)^期数 / [(1+月利率)^期数 - 1] BigDecimal base = BigDecimal.ONE.add(monthlyRate); BigDecimal pow = base.pow(term); // (1+r)^n BigDecimal numerator = loanAmount.multiply(monthlyRate).multiply(pow); BigDecimal denominator = pow.subtract(BigDecimal.ONE); return numerator.divide(denominator, 2, RoundingMode.HALF_UP); } }

血泪经验:

  • 360天计息法:currentDate.plusDays(30)而非currentDate.plusMonths(1),因为银行按每月30天算,不按日历月(2月只有28天会导致利息少算)。
  • 闰年处理:代码中完全规避了闰年判断,因为plusDays(30)对2024-02-01加30天得2024-03-02,对2023-02-01加30天得2023-03-02,天然符合360天法。
  • 精度陷阱:所有BigDecimal运算必须指定RoundingMode.HALF_UP,否则setScale(2)默认UNNECESSARY会抛异常;pow(term)可能产生超长小数,必须在divide()前截断。

4. 运行与调试指南:从零启动到验证一笔真实投标

4.1 环境准备:三步搞定MySQL、ES、Java版本

Step 1:MySQL初始化
执行wanxin-p2p/sql/wanxin_p2p.sql脚本(约12MB),它会创建wanxin_p2p库及18张表。重点表:

  • finance_account:用户账户(available_balance,frozen_balance,total_balance)
  • loan_info:标的主表(loan_amount,rate,term,status,remain_amount)
  • invest_record:投标记录(user_id,loan_id,invest_amount,status)

注意:脚本中loan_info.remain_amount是GENERATED ALWAYS AS (loan_amount - COALESCE(invested_amount,0)) STORED,MySQL 5.7+才支持生成列,低版本需改为触发器。

Step 2:ES索引创建
将wanxin-p2p/es/loan_index_mapping.json内容POST到http://your-es-host:9200/loan_index。若ES启用了X-Pack安全,需在Header加Authorization: Basic [base64(username:password)]。

Step 3:JDK与Maven确认

  • JDK:必须为JDK 8u202+或JDK 11.0.12+(项目用--add-opens参数,低版本不兼容)
  • Maven:3.6.3+,执行mvn clean compile -Dmaven.test.skip=true跳过测试(部分测试依赖未mock的Redis)

4.2 启动命令与Profile激活

项目默认使用devprofile,但后台管理需单独激活adminprofile:

# 启动主应用(提供API) cd wanxin-p2p/wanxin-p2p-admin mvn spring-boot:run -Dspring.profiles.active=dev # 启动后台管理(提供/admin页面) cd wanxin-p2p/wanxin-p2p-admin mvn spring-boot:run -Dspring.profiles.active=admin

提示:adminprofile会加载application-admin.yml,其中server.port=8081,避免与主应用端口冲突。访问http://localhost:8081即可看到Vue后台。

4.3 首笔投标全流程验证:手动生成测试数据

① 准备测试用户与标的

  • 用Postman调用POST http://localhost:8080/user/register注册用户,得到userId=1001
  • 在MySQL中手动插入一条标的:
    INSERT INTO loan_info (id, title, description, rate, term, loan_amount, status, borrower_id) VALUES (1, '小微企业经营贷', '用于日常经营周转', 12.0, 12, 10000.00, 'APPROVED', 2001);

② 发起投标请求

curl -X POST http://localhost:8080/invest/doInvest \ -H "Content-Type: application/json" \ -H "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -d '{"loanId":1,"amount":5000.00}'

③ 验证结果

  • 查finance_account表:user_id=1001的available_balance应减少5000,frozen_balance增加5000
  • 查invest_record表:新增记录,status='INVESTING'
  • 查loan_info表:remain_amount应变为5000.00

若以上全部命中,恭喜,你的P2P资金流已跑通!


5. 常见问题排查:5个真实踩坑记录与解决方案

5.1 现象:投标接口返回{"code":500,"msg":"数据库连接超时"},MySQL日志显示Too many connections

原因:application.yml中未配置MySQL连接池参数,HikariCP默认maximumPoolSize=10,但项目中InvestService、LoanAuditService等高频调用服务未复用连接,导致连接泄漏。
解决:在application-dev.yml中显式配置:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 leak-detection-threshold: 60000 # 检测连接泄漏(毫秒)

5.2 现象:ES搜索返回空列表,但curl http://es-host:9200/loan_index/_search能查到数据

原因:Java客户端未正确设置search_analyzer,title字段用ik_max_word分词,但查询时未指定analyzer=ik_smart,导致搜索词被过度分词。
解决:在LoanSearchService.java的查询构造中,强制指定analyzer:

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, "title^3", "description") .analyzer("ik_smart")); // 关键:指定搜索时用ik_smart

5.3 现象:还款计划生成后,最后一期remaining_principal为-0.01

原因:BigDecimal四舍五入模式错误。setScale(2, RoundingMode.HALF_UP)在principal = monthPayment.subtract(interest)时,若monthPayment=888.49、interest=8.49,principal计算为880.00,但实际应为880.01以补足本金。
解决:在循环末尾强制修正最后一期:

if (i == term) { principal = remainingPrincipal; // 直接取剩余本金 interest = monthPayment.subtract(principal); // 重新计算totalAmount,避免浮点误差 monthPayment = principal.add(interest); }

5.4 现象:后台管理页面/admin报404,但/api/loan/list接口正常

原因:wanxin-p2p-admin模块的pom.xml中,<packaging>为jar,但Vue前端资源未打包进jar,且application-admin.yml未配置静态资源路径。
解决:

  1. 将Vue构建后的dist目录复制到wanxin-p2p-admin/src/main/resources/static
  2. 在application-admin.yml中添加:
    spring: web: resources: static-locations: classpath:/static/,classpath:/public/

5.5 现象:JWT token解析失败,JwtUtil.getUserId()返回null

原因:前端传入的tokenHeader值为Bearer eyJhbG...,但代码中@RequestHeader("token")直接取值,未去除Bearer前缀。
解决:修改InvestController.invest()方法:

String token = requestHeader.replace("Bearer ", ""); // 去除Bearer前缀 Long userId = JwtUtil.getUserId(token);

6. 二次开发实战:给标的搜索增加「按地域筛选」功能(含ES mapping更新与Java代码改造)

6.1 需求分析:为什么地域筛选必须走ES而非MySQL

P2P平台常需支持“找北京地区的小微企业贷”,地域字段(如province,city)在MySQL中是普通VARCHAR,但:

  • 地域有层级(省→市→区),需支持province="北京" OR city="北京";
  • 地域数据量大(全国34省级+333地级+2843县级),MySQLIN查询性能差;
  • 未来要支持“附近N公里标的”,需ES的geo_distance查询。
    因此,必须将地域字段加入ES索引,并保持与MySQL数据一致。

6.2 ES索引改造:新增location嵌套对象与geo_point字段

第一步,关闭旧索引并重建(生产环境请用reindex API):

# 1. 关闭索引 POST /loan_index/_close # 2. 更新mapping,添加location字段 PUT /loan_index/_mapping { "properties": { "location": { "type": "object", "properties": { "province": { "type": "keyword" }, "city": { "type": "keyword" }, "district": { "type": "keyword" }, "geo_point": { "type": "geo_point" } // 为未来地理围栏预留 } } } } # 3. 重新打开 POST /loan_index/_open

第二步,修改MySQLloan_info表,增加province/city字段:

ALTER TABLE loan_info ADD COLUMN province VARCHAR(32) DEFAULT NULL, ADD COLUMN city VARCHAR(32) DEFAULT NULL;

6.3 Java代码改造:同步写入ES与搜索逻辑增强

① 修改LoanInfo实体类,添加地域字段

@Data @Document(indexName = "loan_index") public class LoanInfo { @Id private Long id; private String title; // ... 其他字段 private String province; // 新增 private String city; // 新增 // 新增:ES同步时自动填充location对象 @Field(type = FieldType.Object) private Map<String, Object> location; @PostLoad // JPA生命周期回调,在从DB加载后执行 public void fillLocation() { if (this.province != null || this.city != null) { this.location = new HashMap<>(); this.location.put("province", this.province); this.location.put("city", this.city); } } }

② 修改LoanSearchService.searchLoans(),支持地域过滤

public PageResult<LoanInfo> searchLoans(SearchRequest request) { SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 原有关键词搜索... if (StringUtils.isNotBlank(request.getKeyword())) { sourceBuilder.query(QueryBuilders.multiMatchQuery(request.getKeyword(), "title^3", "description")); } // 新增:地域过滤 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (StringUtils.isNotBlank(request.getProvince())) { boolQuery.must(QueryBuilders.termQuery("location.province", request.getProvince())); } if (StringUtils.isNotBlank(request.getCity())) { boolQuery.must(QueryBuilders.termQuery("location.city", request.getCity())); } sourceBuilder.query(boolQuery); // 分页 sourceBuilder.from((request.getPageNum() - 1) * request.getPageSize()); sourceBuilder.size(request.getPageSize()); SearchRequest searchRequest = new SearchRequest("loan_index"); searchRequest.source(sourceBuilder); // ... 执行搜索并封装结果 }

③ 改造LoanInfoMapper,确保新增字段被查询
在LoanInfoMapper.xml的<resultMap>中添加:

<result column="province" property="province"/> <result column="city" property="city"/>

6.4 数据同步保障:如何让MySQL更新后ES自动刷新

项目未用Logstash或Canal,而是采用应用层双写+失败补偿:

  • 在LoanInfoService.updateLoanInfo()中,先更新MySQL,再调用elasticsearchTemplate.update()更新ES;
  • 若ES更新失败,记录es_sync_log表(table_name,record_id,status=FAILED,retry_count),并由定时任务每5分钟扫描重试。

从那以后我每次加新字段到ES,都强制走一遍「MySQL改表 → ES建mapping → Java实体加字段 → 同步逻辑补全 → 补偿任务兜底」这四步,漏一步就等着线上搜索崩。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询