资讯详情

资讯详情

SpringBoot+Vue构建农业金融融销平台:从订单到融资的完整实践

简介一套采用B/S架构的农业金融融销一体化平台源码基于SpringBoot与Vue开发面向农业金融产品经理、Java后端与前端工程师以及希望了解农业数字化转型方案的开发者。平台以融资服务为核心配套信息共享、用户交互、技术指导与个性化农业增值服务同时为投资方提供多元化投资渠道重点解决小额融资难与产品同质化问题。压缩包共31个文件含10个Java源文件、10个class文件、7个XML与2个YAML配置文件另含说明文档及Git忽略文件Java源码承载业务逻辑XML/YAML管理运行参数整体约50KB结构清晰便于通读。当前已有579人学习下载适合作为农业金融领域中小型前后端分离项目的设计与实现参考其中RESTful API与Vue交互方式可帮助开发者理解SpringBootVue整合开发流程。工程按控制层、服务层、配置类等层次划分业务逻辑与调用关系直观适合二次开发与学习研究。1. 农业融销平台为什么绕不开SpringBootVue这套组合农资经销商的账期通常压得很死上游厂家要求现款现货下游农户又想等卖粮再结账中间三个月到半年的资金缺口全靠经销商自己扛。传统银行信贷进不了这么细的场景于是有了“农业金融融销一体化”这种把商品销售和供应链金融绑在同一个系统里的做法。这个标题看起来像是一个项目名实际上是在描述一个典型的前后端分离业务系统农户或经销商在平台上发起采购订单平台依据历史交易数据给授信额度订单履约的同时自动生成融资单后续按还款计划逐期回款。SpringBoot承担的是领域服务和资金流转的稳定性Vue承担的是台账、审批、看板这类高频交互页面。选择这套组合不是因为它时髦而是因为这个场景大量操作发生在内网、平板和低带宽环境SpringBoot的轻量部署和Vue的响应式交互都合适。适合的读者是有Java后端基础、想快速搭一套带实际业务闭环系统的工程师看完可以直接照着把骨架和核心代码搭起来而不是停留在概念层。2. 农业金融融销一体化平台的领域建模从订单到融资的数据流2.1 融销平台的四个核心实体与数据流向这类平台无论叫“融销一体化”还是“供应链金融中台”核心实体逃不出四个供销订单、融资申请单、还款计划、授信账户。理解数据流比先写代码重要因为后续所有接口设计都是围着这条链路转的。订单模块先落一笔真实的销售流水平台拿到订单后判定是否在授信范围内允许融资的订单进入融资单创建流程融资单一旦审核通过资金系统按比例放款同时生成分期还款计划。数据流向中有一个容易被忽略的点授信账户不是一笔预存款而是一个可用的额度上限。还款计划每还一期授信额度就恢复一部分而不是全部还完才恢复。这个规则直接影响还款接口里额度更新的SQL写法也影响前端页面上“可用额度”这个数字的计算。2.2 订单表与融资单表字段设计直接决定风控可执行性订单表不必搞太复杂但几个关键字段不能省客户ID、商品分类、订单总金额、已付金额、赊销金额、订单状态。赊销金额是进入融资判断的依据平台只对“已交货未回款”的部分提供资金周转不做全款融资。融资单表则要记录融资比例、融资金额、年化利率、放款状态、还款方式。这里建议主键直接用分布式ID而不自增避免融资单号被猜测越权查询。字段类型方面有硬性要求涉及钱的列一律用decimal(14,2)不允许float或double。浮点计算的误差在单笔上不明显累计到资金对账时就成了查不完的差异。还款计划表建议加一个“已还本金”“已还利息”字段而不是只存“应还金额”这样逾期计算时可以直接用剩余本金乘罚息比例不用再查历史流水。2.3 建表DDL金额decimal(14,2)、状态字段预留扩展位融资单表是整套系统的核心表先把它落干净。以下SQL可以直接放进MySQL 5.7以上版本执行CREATE TABLE finance_order ( id bigint NOT NULL COMMENT 分布式ID主键, fin_no varchar(32) NOT NULL COMMENT 融资单号唯一索引, order_id bigint NOT NULL COMMENT 关联供销订单ID, customer_id bigint NOT NULL COMMENT 融资客户ID, fin_amount decimal(14,2) NOT NULL COMMENT 融资金额, fin_ratio decimal(5,2) NOT NULL COMMENT 融资比例如80.00表示80%, annual_rate decimal(5,2) NOT NULL COMMENT 年化利率%, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审 1已放款 2还款中 3已结清 4逾期 5已拒绝, apply_time datetime NOT NULL COMMENT 申请时间, audit_time datetime DEFAULT NULL COMMENT 审核时间, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_fin_no (fin_no), KEY idx_customer_status (customer_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT融资申请单;这段DDL里三个细节需要注意。第一fin_no建唯一索引是幂等写入的基础重复提交时数据库会拒绝第二条比应用层判断更可靠。第二status用tinyint不用varchar状态流转判断写在Java枚举里数据库只存数字省空间也方便扩展。第三version字段不是给页面展示用的它承担并发控制后面讲放款接口时要用到它。3. SpringBoot后端骨架工程配置与融销核心接口实现3.1 SpringBoot版本选择与工程结构做农业金融这类对稳定性要求高的业务我一般不建议追最新大版本。SpringBoot 2.7.18是2.x的收尾版本能完美兼容MyBatis-Plus、Redisson、XXL-Job这些常用组件同时具备完整的CVE修复记录适合生产环境。如果你用的是JDK17直接上SpringBoot 3.2也没问题但要注意javax包名改成了jakarta老代码迁移时会多一步批量替换。工程结构不要按三层分包就完了建议按业务域划分避免融资和订单代码全部堆在service包里com.nongye.finance ├── common -- 统一返回、异常处理、常量 ├── order -- 订单领域 ├── finance -- 融资领域 ├── repay -- 还款计划领域 ├── customer -- 客户与授信 └── config -- 数据源、Redis、MybatisPlus配置3.2 application.yml中必须显式配置的三类项SpringBoot的自动配置很方便但默认值未必适合融资场景。数据源连接池如果沿用默认的HikariCP参数高峰期申请融资时连接不够会直接报connection is not available。既然要演示可复现把完整配置写在这里复制即用server: port: 8080 servlet: context-path: /fin spring: datasource: url: jdbc:mysql://localhost:3306/nongye_fin?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 5000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id连接池的maximum-pool-size设20能满足绝大多数中小规模农贸场景不要盲目调到100每个连接背后都有一个MySQL线程线程过多反而增加上下文切换开销。map-underscore-to-camel-case开启后数据库的fin_amount会自动映射到实体的finAmount属性无需写一堆TableField注释。3.3 融资申请接口幂等、金额校验与事务回滚融资申请是平台最核心的写操作先写Controller再写Service实现。Controller只做参数接收和统一返回不放业务逻辑方便后续加权限注解时统一拦截。RestController RequestMapping(/finance) public class FinanceController { Resource private FinanceApplyService financeApplyService; PostMapping(/apply) public ResultLong apply(RequestBody Valid FinanceApplyDTO dto) { return Result.success(financeApplyService.doApply(dto)); } }Service里需要注意三个点第一幂等控制依赖fin_no唯一索引重复调用时捕获DuplicateKeyException在接口层面返回“请勿重复提交”而不是抛500。第二融资金额必须小于等于订单赊销金额乘以融资比例这个校验在事务内做防止多线程同时读到相同额度造成超融。第三事务内抛出RuntimeException才会回滚try-catch后不重新抛出是事务失效最常见的原因。Service RequiredArgsConstructor public class FinanceApplyServiceImpl implements FinanceApplyService { private final FinanceOrderMapper financeOrderMapper; private final SaleOrderMapper saleOrderMapper; Override Transactional(rollbackFor Exception.class) public Long doApply(FinanceApplyDTO dto) { SaleOrder order saleOrderMapper.selectById(dto.getOrderId()); if (order null) { throw new BizException(订单不存在); } // 计算最大可融金额赊销金额 * 融资比例 BigDecimal maxFinance order.getCreditAmount() .multiply(dto.getFinRatio()) .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); if (dto.getFinAmount().compareTo(maxFinance) 0) { throw new BizException(融资金额超出订单可融上限); } FinanceOrder financeOrder new FinanceOrder(); financeOrder.setFinNo(generateFinNo()); financeOrder.setOrderId(order.getId()); financeOrder.setCustomerId(order.getCustomerId()); financeOrder.setFinAmount(dto.getFinAmount()); financeOrder.setFinRatio(dto.getFinRatio()); financeOrder.setAnnualRate(dto.getAnnualRate()); financeOrder.setStatus(FinanceStatusEnum.PENDING.getCode()); financeOrderMapper.insert(financeOrder); return financeOrder.getId(); } private String generateFinNo() { return FIN System.currentTimeMillis() RandomUtil.randomNumbers(4); } }金额比较用了compareTo而不是equals这是BigDecimal比较的硬性规范。equals会比较精度1.0和1.00会判断为不相等而业务上它们显然是同一个数。divide方法里指定了RoundingMode.HALF_UP避免除不尽时抛ArithmeticException。4. Vue前端落地从环境搭建到融销业务台账4.1 用Vite创建融销工作台前端骨架Vue3选Vite作为构建工具冷启动比Webpack快一个量级日常开发体验明显更好。创建项目时直接用npm命令不需要手动配置webpack那一堆loadernpm create vitelatest nongye-front -- --template vue cd nongye-front npm install npm install vue-router4 element-plus axios dayjs安装依赖后把Element Plus按需引入改成完整引入一个main.js里搞定不要在生产环境用按需引入插件那些插件偶尔会漏掉Table组件的子组件样式排查起来很费时间import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/es/locale/lang/zh-cn import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus, { locale: zhCn }) app.use(router) app.mount(#app)4.2 路由带参从订单列表跳到融资单详情用户从订单列表点“申请融资”时需要把订单ID和客户ID带到融资申请页。直接拼接字符串再JSON.parse是坏习惯Vue Router原生支持对象形式传参刷新页面后参数依然保留在URL里可分享可复盘// 订单列表页跳转 router.push({ name: FinanceApply, query: { orderId: row.id, customerId: row.customerId, creditAmount: row.creditAmount } }) // 融资申请页读取参数 const route useRoute() const orderId route.query.orderId为什么这里不用params而是用query因为params传参在页面刷新后会丢失用户从申请页刷新就直接变成空订单号体验很差。query参数出现在URL地址栏中对于订单号这类非敏感信息完全够用。涉及客户身份证、手机号等隐私数据要跳到详情页后再通过接口获取避免明文挂在URL上被日志系统记录。4.3 封装Axios与Element Plus账表格组件请求封装是前端工程化的第一道坎。统一在Axios拦截器里处理token携带、业务错误码提示、HTTP 401跳转登录业务代码里就不用每个请求重复写错误处理逻辑import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default service账务组件用el-table展示核心是格式化方法和汇总行。融资比例、还款状态这类字段要给后端返回的原始数字做映射不能直接把0、1、2显示给农户看。template el-table :datarepayList show-summary :summary-methodgetSummaries el-table-column proprepayDate label应还日期 width120 / el-table-column propprincipal label应还本金 width120 template #default{ row } {{ formatMoney(row.principal) }} /template /el-table-column el-table-column propinterest label应还利息 width120 / el-table-column label状态 width100 template #default{ row } el-tag :typerepayStatusMap[row.status].type {{ repayStatusMap[row.status].text }} /el-tag /template /el-table-column /el-table /template script setup const repayStatusMap { 0: { text: 待还款, type: warning }, 1: { text: 已结清, type: success }, 2: { text: 已逾期, type: danger } } /scriptshow-summary配合summary-method实现表尾合计这个合计是后端返回数据在前端做累加只适合展示不能作为对账凭证。真正的对账数据要以后端查询接口返回的汇总值为准前端合计数字仅供参考这是开发时要明确告诉业务方的。5. 融资放款与还款结算事务边界与状态机设计5.1 融资状态机与流转约束融资单不是简单的CRUD审核、放款、还款、逾期这条链路中每一步都对应一个状态迁移。最稳妥的实现方式是在Java枚举中定义状态和允许的流转方向而不是在Service里写一堆if-else判断可以走哪个分支。public enum FinanceStatusEnum { PENDING(0, 待审核), LENDING(1, 已放款), REPAYING(2, 还款中), SETTLED(3, 已结清), OVERDUE(4, 逾期), REJECTED(5, 已拒绝); private final int code; private final String desc; public boolean canTransitTo(FinanceStatusEnum target) { switch (this) { case PENDING: return target LENDING || target REJECTED; case LENDING: return target REPAYING; case REPAYING: return target SETTLED || target OVERDUE; default: return false; } } }状态机的核心价值是防止非法跳转。没有约束时一个还款中的融资单可能被手动改成已结清资金账目瞬间对不上。有了枚举约束每次流转都调用canTransitTo做校验不合法直接抛业务异常。审批流工具Flowable也能做同样的事但这个场景状态少、流转路径明确用枚举成本最低不需要引入工作流引擎。5.2 并发放款下的乐观锁幂等策略资金操作最怕重复放款。两个管理员同时审核同一张融资单如果放款接口没有做并发控制就可能扣两次客户额度、生成两笔放款记录。解决思路用数据表自带的version字段加乐观锁更新时带上旧版本号影响行数为0说明已被其他人处理。UPDATE finance_order SET status 2, audit_time NOW(), version version 1 WHERE id #{id} AND status 0 AND version #{version}MyBatis-Plus里直接用UpdateWrapper拼条件Transactional(rollbackFor Exception.class) public void auditPass(Long financeId) { FinanceOrder order financeOrderMapper.selectById(financeId); if (!FinanceStatusEnum.PENDING.canTransitTo(FinanceStatusEnum.REPAYING)) { throw new BizException(当前状态不可放款); } int rows financeOrderMapper.update(null, new LambdaUpdateWrapperFinanceOrder() .eq(FinanceOrder::getId, financeId) .eq(FinanceOrder::getStatus, FinanceStatusEnum.PENDING.getCode()) .eq(FinanceOrder::getVersion, order.getVersion()) .set(FinanceOrder::getStatus, FinanceStatusEnum.REPAYING.getCode()) .set(FinanceOrder::getAuditTime, LocalDateTime.now()) .setSql(version version 1)); if (rows 0) { throw new BizException(操作冲突请刷新后重试); } // 生成还款计划 generateRepayPlan(financeId); }放款和生成还款计划必须在同一个事务里。如果先更新状态成功再生成还款计划时抛异常事务整体回滚不会出现“状态显示已放款但查不到还款计划”的脏数据。Transactional默认只回滚RuntimeException这里明确指定rollbackFor Exception.class把受检异常也纳入回滚范围。5.3 逾期待还款计划的定时扫描策略逾期不能靠人工盯着看得靠定时任务每天凌晨扫描。用Spring自带的Scheduled就能跑不需要上XXL-Job。扫描逻辑是找出“应还日期小于今天且状态为待还款”的还款计划把对应的融资单状态改成逾期同时计算罚息。Component public class OverdueScheduledTask { Resource private RepayPlanMapper repayPlanMapper; Scheduled(cron 0 30 0 * * ?) public void scanOverdue() { ListRepayPlan overduePlans repayPlanMapper.selectList( new LambdaQueryWrapperRepayPlan() .eq(RepayPlan::getStatus, 0) .lt(RepayPlan::getRepayDate, LocalDate.now())); for (RepayPlan plan : overduePlans) { plan.setStatus(2); plan.setOverdueDays(calculateOverdueDays(plan)); repayPlanMapper.updateById(plan); } } }定时任务一定要分布式锁保护。生产环境通常部署两个实例做高可用同一个凌晨扫描任务会在两台机器上同时跑没有锁会导致重复更新和重复短信通知。常见做法是配合Redis的SETNX拿锁拿不到锁的实例直接跳过。6. 落地上线前必做的三个SpringBoot配置调整6.1 生产环境Multi-Profile配置与yml口令脱敏开发环境用本地MySQL生产环境用云数据库连接串、账号、密码都不能写死在一份yml里直接用。SpringBoot原生的多Profile机制能解决这个问题application.yml只放公共配置application-dev.yml和application-prod.yml各自维护环境专属项启动时用--spring.profiles.activeprod指定跑哪套。密码放在配置文件本身就是安全隐患常见做法是改成环境变量占位符数据库密码在运维平台或KMS里统一管理。spring: datasource: password: ${DB_PASSWORD}6.2 三个必调参数连接池上限、事务超时、Jackson时区配置项推荐值调整理由spring.datasource.hikari.maximum-pool-size20默认10撑不住农业销售旺季的并发融资申请spring.transaction.default-timeout30防止接口等待数据库行锁导致线程堆积spring.jackson.time-zoneGMT8不设置时时间会差8小时前端展示全错事务超时参数建议显式声明SpringBoot默认不限制事务执行时间一旦数据库出现慢SQL事务占用的数据库连接会一直不释放慢慢耗尽连接池。30秒是一个平衡值既能容忍正常业务的批量操作又能防止异常情况拖垮整个服务。Jackson的时区问题最隐蔽本地开发环境是东八区没感觉服务端部署到云主机且系统时区为UTC时所有时间字段查询出来都少8小时这个参数属于上线前必查项。6.3 用日志定位状态机异常和SQL执行痕迹上线前一天把日志级别调成DEBUG跑一遍全流程重点看#{param}替换后的真实SQL能当场发现字段映射错误。生产环境只开INFO否则高并发下日志量会拖慢接口响应。排障时用tail -f配合grep Exception快速定位遇到“操作冲突”提示直接查fin_no关联的version字段变化轨迹确认是否被其他用户先行更新。验证整个链路是否正常最快的方式是下单、融资、放款、还款跑一遍完整业务流最后核对还款计划表和授信账户余额是否一致。数据对上了这套系统才算真正可用。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →