ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+Android水务缴费系统开发实战与复盘

SpringBoot+Vue3+Android水务缴费系统开发实战与复盘 不废话直接聊项目。这次要复盘的是一套“springbootvue3基于Android的水务居民自来水缴费管理系统”典型的多端业务系统后端Spring BootWeb管理端Vue3移动端Android原生。简单说就是抄表员不用再拿着纸笔上门催费居民也不用跑营业厅排队手机上就能查账单、缴费、看历史记录后台还能管用户、管表计、管账单、管抄表任务、统计营收。整套东西拆开看不复杂但把“多端”“支付”“权限”“消息推送”这几个点串起来涉及到的坑一点也不少。如果你是打算做毕设或者刚接手一个类似的“Java后端 Vue管理端 Android端”小项目这篇文章应该能帮你少走不少弯路。我会按实际开发顺序把架构设计、核心表结构、JWT鉴权、支付模拟、抄表对账、Android端联调、Vue3后台实现这些环节逐个讲透最后再甩一份备查清单和常见问题速查表。每个环节都附上我踩过的坑和验证过的写法。1. 项目整体设计与架构思路1.1 多端角色梳理谁在用这套系统任何管理系统第一步都不是写代码而是搞清楚“谁在用”。水务缴费系统的核心角色不多但权限边界必须清晰否则后面越做越乱。超级管理员管后台全部包括运营人员账号、分区数据、系统配置权限最高一般只留给项目负责人或平台运营。分区/营业厅管理员管理本区域的用户档案、抄表员、账单复核相当于片区小老板。抄表员在Android端查看派给自己的抄表任务录入水表读数上传现场照片提交异常情况。居民用户通过Android端或H5自助绑定户号查看账单、在线缴费、查历史缴费记录。财务角色可选查看每日/每月营收报表、导出对账数据。在实际实现里我建议把这些角色抽象成user表里的role字段或者用role_id关联角色表。不要一上来就上RABC那套复杂的权限模型——对毕设和中小型项目来说一个PreAuthorize(hasRole(ADMIN))加上后端接口级校验就够了。核心规则只有两条抄表员不能看到用户家庭住址用户不能看到抄表员的任务列表。做到这两条权限模型就算过关。1.2 技术栈选型的为什么这个项目的技术选型基本是把当前Java全栈的热门组合凑齐了但每个选择背后都有实际考虑Spring Boot 2.7.x生态最稳的版本。别一上来就追3.x很多老教程、毕业设计代码都是2.x遇到报错搜资料会省心很多。如果非要上Spring Boot 3记得JDK要用17且部分第三方starter比如一些旧版代码生成器会不兼容。MyBatis-Plus单表CRUD神器内置分页插件、逻辑删除、字段自动填充对“管理系统”这种充满重复增删改查的代码量能砍掉一半。Vue3 Element PlusVue3的组合式APIsetup写业务逻辑比Vue2的Options API更清晰尤其是表格、表单、弹窗这种高频场景。Element Plus的组件库直接解决后台管理80%的界面问题。Android原生Java/Kotlin用Android Studio开发原生应用。为什么不用uni-app因为标题明确写了Android而且原生在调用摄像头、定位、离线缓存方面的容错率更高抄表场景经常在户外信号不好原生比H5壳稳。MySQL 8.0业务数据不算复杂两张核心表拆好就够用。用InnoDB引擎字符集定utf8mb4别问为什么问就是避免生僻字和emoji乱码。RedisSession共享、验证码存储、高频读缓存。缴费系统的高频读是“账单查询”这个接口压到缓存里效果非常明显。支付真实对接支付宝/微信需要营业执照很多毕设或练习项目不具备这个条件。我的做法是做一个支付模拟器——生成本地支付订单模拟支付成功回调同时预留支付渠道接口后期接入真实SDK时只需替换实现类。这套组合的实际体积大概在1万行代码左右其中Spring Boot后端占4000~5000行Vue3管理端占3000行Android端占2000~3000行是一个人能在一个月内完成的工作量峰值。再复杂就超纲了。1.3 系统架构与工程结构整个系统采用“服务端 Web管理后台 Android移动端”的三角架构三者之间全部通过RESTful API通信不需要服务端渲染页面。后端工程结构建议按业务模块分包而不是按技术类型分包。很多人习惯建controller、service、mapper三个包然后把所有类丢进去那样项目一大就完蛋。我推荐这样com.water.system ├── common // 通用返回结果、异常处理、常量 ├── config // Redis、MyBatis-Plus、拦截器、跨域配置 ├── controller // 接收请求只做参数校验和结果包装 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 前端请求参数对象 ├── vo // 后端返回视图对象 └── utils // JWT、日期、Excel导出等工具关键点在于dto和vo一定要和entity分开。否则前端传参直接绑定实体对象多传一个roleADMIN就能越权返回给前端时也会把密码hash这些敏感字段带出去。用vo做字段裁剪是安全底线。2. 核心功能模块与数据库设计2.1 表结构设计这些表一张都不能少我围绕业务把表拆成三组用户权限组、账单业务组、运维支撑组。见过不少同学把账单和用户表耦合在一起、地址字段直接塞用户表结果一到“一户多表”就傻眼。水务场景有一个天然门槛户、表、地址三者不是一一对应的必须拆开。t_user用户账号表——id、手机号、密码BCrypt加密、昵称、角色role、状态、创建时间。t_customer居民档案表——姓名、手机号、证件号、地址ID、用户ID绑定登录账号。一个人名下可以绑定多套房所以是多人对多房对多表。t_address地址表——省市区、街道、小区、楼栋、门牌号、分区标识一个分区对应一个营业厅。t_water_meter水表档案表——表编号唯一、用户ID或地址ID、表类型机械表/智能表、初始读数、安装日期、状态。t_receipt账单表——账单编号、表ID、期数如202502、上期读数、本期读数、用水量、单价、金额、状态未缴/已缴/欠费/销账、缴费时间、支付流水号。t_payment_order支付订单表——订单号、账单ID、用户ID、金额、渠道支付宝/微信/模拟/柜台代收、状态待支付/成功/失败/退款、回调时间。t_read_task抄表任务表——抄表员ID、表ID、任务日期、状态待抄/已完成/异常、现场读数、照片URL、备注。t_notice公告表——标题、内容、类型催缴/停水/公告、发布时间、发布人。t_operation_log操作日志表——用户ID、操作类型、请求参数、IP、耗时、结果。账单表是这套系统的核心。设计时我踩过一个坑最初把“上期读数”“本期读数”设计成了两个可空字段结果发现漏抄时本期读数是空的但账单还是要生成——按预估用量生成。最后我加了一个字段reading_source标记本期读数是“实测”还是“估抄”这样用户看到账单时能知道为什么这期数值和平时不一样投诉率低很多。2.2 账单生成与销账业务流程账单业务的完整闭环是这样的管理员在后台创建抄表任务指定抄表员、片区、任务月份。抄表员登录Android端查看“我的任务”逐条录入水表读数支持拍照留存。读数超过一定阈值比如上月读数的3倍时系统提示“读数异常请重新确认”。抄表员提交读数后后端触发账单生成逻辑用水量 本期读数 - 上期读数应收金额 用水量 * 单价阶梯计价需分段计算。如果本期未抄到表系统按“近3期平均用水量”生成估抄账单并在账单上标注“估抄”抄表员下次抄表时校验若差异较大则做冲正。用户收到短信/App内消息推送催缴通知在Android端查看账单详情点击缴费生成支付订单。模拟支付回调成功后修改账单状态为“已缴”写入缴费流水并给用户推送“缴费成功通知”。财务在后台查看对账报表按日/按片区统计应收、实收、欠费率。这里看似简单但有个容易漏掉的细节阶梯水价。很多地方水价是分档的比如居民用水第一阶梯每月20吨以内每吨2.8元第二阶梯20~30吨每吨4.2元超过部分6.5元。计算时不能拿总量乘单价要按阶梯累计扣除额度算。如果你在毕设答辩时主动讲出这一层老师会认为你真的调研过业务。2.3 支付模块本地模拟与真实渠道预留真实支付需要商户号、证书、签约个人开发者根本拿不到。我的方案是做一个“支付渠道策略模式”定义统一接口PaymentChannel方法包括createOrder和handleCallback。MockPaymentChannel实现类负责本地模拟用户点击缴费后系统生成订单号格式可用日期随机数返回“支付二维码”二维码内容就是一个模拟收银台链接——在浏览器打开后显示订单金额、账单号、一个“确认支付”按钮。实战中我会再做一个alipayChannel的骨架类注释写明AppId、私钥、支付宝网关等真实配置从哪里填。这样代码结构上就具备了扩展性。支付回调是安全重点。真实环境里支付宝回调是可以伪造的所以要验签。模拟环境也要做一件事防止用户绕过支付直接调用“修改账单状态”接口。我的做法是账单状态只能由handleCallback方法触发流转PaymentChannel接受到自身生成的确认码模拟环境中是UUID后才能把订单置为成功并更新账单状态。第三方无法用普通HTTP请求伪造。3. Spring Boot后端实现核心要点3.1 统一返回结果与全局异常处理前后端分离项目最容易被忽略的就是“返回格式不统一”。有的接口返回{code: 200, data: {...}}有的接口直接返回对象前端每个请求都要单独处理后期维护痛苦到怀疑人生。我定义的统一返回结构是{ code: 200, message: success, data: {} }code约定200成功400参数错误401未登录/Token失效403无权限500系统内部错误。不是所有的业务失败都返回500比如“余额不足”这种业务错误就应该返回一个业务错误码比如2001前端拿到后弹toast“余额不足”而不是大红屏。全局异常处理用RestControllerAdvice捕获两类异常BizException业务异常message可直接展示给用户和Exception未知异常message统一为“系统繁忙”异常堆栈打日志。这样用户体验和排查效率都兼顾。3.2 JWT登录鉴权无状态Token实战Spring Boot Vue3 Android三端通用的鉴权方案我用的是JWT。核心逻辑不复杂但有几个细节容易被坑// 生成Token有效期设置为2小时 String token Jwts.builder() .setSubject(userId.toString()) .claim(role, user.getRole()) .claim(nickname, user.getNickname()) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();注意几点secretKey不能在代码里写死要放配置文件并通过环境变量注入。写死的话仓库代码泄露所有用户的Token可以被伪造。Token有效期2小时用户操作到一半过期会体验很差。我的做法是后端提供一个/auth/refresh接口前端拦截到401时自动携带旧Token调用刷新接口换取新Token后重试原请求。Android端用拦截器处理Vue3端在Axios响应拦截器处理。服务端用拦截器解析Token并把userId放到ThreadLocal或直接用ServletRequest的attribute业务层不需要再从参数里传用户ID。登出JWT是无状态的服务端没法真正让Token失效。我的处理方式是前端删Token同时后端把当前Token的jti加入Redis黑名单直到过期时间。这个方案在小项目里完全够用不用引入复杂的分布式会话。3.3 MyBatis-Plus代码量减半的秘诀MyBatis-Plus把单表CRUD的操作简化为“继承BaseMapper”。但真正让代码质量提升的是这三个功能逻辑删除在实体字段上加TableLogic删除操作自动变成update set deleted 1。别把用户表的数据物理删掉账单表还有外联关系。而且这也符合“居民换房不注销档案”的业务习惯。自动填充create_time和update_time字段用MetaObjectHandler自动填充不用每次插入时手动set当前时间。数据库表里也设置DEFAULT CURRENT_TIMESTAMP双保险。分页插件PaginationInnerInterceptor配置后QueryWrapper带Page对象即可返回分页结果。注意MySQL的分页参数offset,limit底层是PreparedStatement不会有SQL注入问题。关联表查询不要偷懒用TableField(exist false)搞“虚拟字段”。账单列表要显示“用户名”正确的做法是SELECT r.*, c.name FROM t_receipt r LEFT JOIN t_customer c ON r.customer_id c.id在Mapper里写XML或注解SQL完成。MyBatis-Plus的TableField(exist false)只用来做非DB字段的数据聚合而不是做关联查询否则数据出错了你根本分不清是SQL问题还是实体映射问题。3.4 Redis的三种用法缓存、限流、防重复提交这个项目里Redis不是摆设我用它做了三件事第一短信验证码存储。登录/找回密码时生成6位验证码存Rediskey为sms:{手机号}有效期5分钟。发送前检查是否已有验证码有则提示“请勿频繁发送”。这是拦截短信轰炸的简易方案。第二高频接口缓存。查询“我的账单列表”“公告列表”这类读多写少的接口用Cacheable Redis做缓存。账单状态变更时删除对应缓存就能保证一致性。缓存key建议包含用户ID和页码比如receipt:list:{userId}:{current}:{size}。第三防重复提交。缴费这个操作最怕用户手抖点了两次“确认支付”生成了两个订单。我的做法是用户点击缴费时后端先用Redis的SETNX设置一个锁key为pay:{userId}:{billId}同时设置10秒过期。如果SETNX返回false说明已有操作在进行直接返回“请勿重复提交”。这比单纯前端按钮置灰可靠——前端可以绕过Redis锁绕不过。4. Vue3管理后台开发实录4.1 项目初始化Vite Vue3 TypeScriptVue3前台我直接上create-vite脚手架用TypeScript写业务代码。命令npm create vitelatest water-web -- --template vue-ts需要注意Node.js版本——Vite 5要求Node 18如果你本机是16会报错error:0308010C:digital envelope routines::unsupported。这种情况不要死磕Vite版本直接升级Node到LTS版本20最省事。装依赖时顺手把这几样装上npm install element-plus element-plus/icons-vue axios pinia vue-router npm install -D sassElement Plus按需导入别全量引入。全量引入虽然省事但打包出来体积大一倍多而且首屏体验明显变慢。用unplugin-auto-import和unplugin-vue-components两个插件自动导入组件和API。4.2 路由与权限控制管理后台的页面结构一般是登录页、布局页、各业务页。路由用vue-router的createWebHashHistoryHash模式——部署到任何静态服务器都不需要额外配置刷新不会404。如果后端和前端同域部署也可以考虑用createWebHistory但要配置服务器回退到index.html。权限控制的核心在路由守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })但页面按钮级权限怎么控制我的方案是账号登录后后端返回一个permissions: string[]数组比如[system:user:add, system:user:delete]前端定义Vue自定义指令v-permission判断当前用户权限数组里是否包含该权限码不包含则移除DOM元素。这样“超级管理员”和“普通管理员”看到的不一样的操作按钮。4.3 表格页面的标准写法抄表任务管理的完整案例以“抄表任务管理”页面为例这类页面的标准套路是搜索表单 表格 分页 编辑弹窗。搜索表单不需要单独写在el-form里然后手动绑定数据我比较推荐在setup里直接管理一个queryParams响应式对象const queryParams ref({ pageNum: 1, pageSize: 10, readerName: , status: , dateRange: [] })表格数据加载用一个getList函数在onMounted时调用分页变化时重新调用搜索按钮点击时重置pageNum1再调用。这就是所谓的“三板斧”。有一个提高代码复用率的细节所有列表查询响应结构都统一为{ records, total }对应的TS类型也定义成泛型interface PageResultT { records: T[] total: number }Excel导出是管理后台的刚需。我用的方案是后端生成Excel字节流用EasyExcel前端用responseType: blob接收创建URL.createObjectURL后模拟a标签下载。网上很多教程把导出做成了“后端生成下载链接前端window.open”这在带Token的鉴权系统里是行不通的——window.open不会携带Authorization头。4.4 ECharts统计报表的常见坑营收统计用ECharts做柱状图/折线图很常见。Vue3里用vue-echarts基于ECharts的Vue封装比较顺手但有一个极容易踩的坑pxtorem对ECharts没起到效果。原因是ECharts的canvas是在ResizeObserver回调里重新绘制的pxtorem这种样式转换工具只处理CSS样式不会处理canvas内部的绘图尺寸。解决办法有两种一是图表容器宽度用百分比高度用固定数值或按窗口高度计算二是监听窗口尺寸变化调用图表实例的resize()方法。千万不要指望响应式框架自动帮你搞定canvas缩放。5. Android端开发与三端联调5.1 Android Studio环境与项目结构Android端用Kotlin Material Design组件开发。环境方面有两点建议第一Android Studio安装时SDK路径不要用含中文的目录否则编译时经常莫名其妙报错。第二下载依赖时如果发现gradle拉取非常慢在build.gradle里配置阿里云镜像仓库repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://maven.aliyun.com/repository/public } }工程分包按功能来ui放Activity/Fragment、adapter放RecyclerView适配器、network放Retrofit接口定义和数据模型、utilsSharedPreferences工具、日期工具等。5.2 登录态保持与请求拦截器Android端请求库我用的Retrofit OkHttp。登录后把Token存到SharedPreferences然后通过OkHttp的Interceptor为每个请求添加Authorization: Bearer {token}头class AuthInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val token SharedPrefsUtil.getToken() val request chain.request().newBuilder() .addHeader(Authorization, Bearer $token) .build() return chain.proceed(request) } }关键在于Token过期后的自动续期。我封装了一个TokenAuthenticator当后端返回401先调用刷新Token接口成功后保存新Token再用新Token重试原请求如果刷新也失败则跳转登录页。这个逻辑不复杂但千万注意不要放在onError里写——那样会出现多个请求同时401、同时刷新Token的情况导致其中一个刷新成功另一个又把Token刷新成旧的。用Authenticator接口是同步阻塞的多个401会排队等待不会并发。5.3 抄表任务中的相机调用与图片上传抄表的现场照片上传Android端调用相机拍照然后压缩上传。有几个坑值得记录Android 7.0及以上访问相机返回的照片需要FileProvider否则抛FileUriExposedException。核心配置是AndroidManifest.xml里加provider并配置对应的file_paths.xml。热搜词里那一堆content://...fileprovider报错基本都源于此。相机原图分辨率极高2000万像素以上直接上传会非常慢而且容易OOM。压缩逻辑先BitmapFactory.Options.inSampleSize采样压缩再用Bitmap.compress输出为JPEG质量设为80。上传用OkHttp的MultipartBody构造多部分请求后端用Spring Boot的MultipartFile接收存到本地目录或对象存储。注意Nginx部署时要调大client_max_body_size默认1MB会导致超过600KB的图片上传失败。5.4 进度条、Notification与用户体验细节Android端加载数据时进度条是标配。我用SwipeRefreshLayoutRecyclerView实现下拉刷新 列表加载。刚开始网络慢时用户会以为点击没反应所以按钮点击后要先禁用按钮并显示ProgressBar同时设置连接超时OkHttp默认10秒够了但弱网环境建议单独对图片上传接口放宽到30秒。缴费成功后要发系统通知。Android 8.0以上需要用NotificationChannel否则通知不显示。我用的是前台服务创建的渠道但注意不要为了定时检查账单做常驻后台服务——国内手机厂商会杀进程这事无解。比较务实的方案是App启动时请求后端“是否有未读通知”有则存本地并弹通知只有用户主动在App内操作时才用WorkManager做短时任务检查。5.5 Android模拟器与后端联调联调阶段最常见的坑是模拟器/真机访问不到本机的后端接口。Android模拟器访问本机不能用localhost要用10.0.2.2。因为模拟器是一个独立的虚拟设备localhost指向的是模拟器自己。真机调试需要保证电脑和手机在同一局域网后端启动时设置server.address0.0.0.0手机访问电脑的局域网IP如http://192.168.1.101:8080。线上测试环境后端部署到公网服务器如果只有IP没有域名Android端访问HTTP接口没问题但如果后端用了HTTPS且证书不是正规CA签发的Android会直接拦截——需要让后端配正规证书或在开发阶段用network_security_config允许明文流量。跨域问题只存在于浏览器端Vue3 dev server访问后端APIAndroid原生不存在跨域限制。所以联调时要分清前端Vue3报CORS是后端没配跨域Android端报网络错误先看IP、端口、防火墙。6. 常见问题与排查技巧实录6.1 Spring Boot后端高频报错Caused by: java.sql.SQLSyntaxErrorException: Unknown column启动报“未知列”一般是实体类字段和数据库字段映射不匹配。MyBatis-Plus默认开启下划线转驼峰如果实体字段是customerName数据库字段必须是customer_name。不想改数据库的话在实体字段上加TableField(customer_name)。Invalid bound statement (not found)Mapper接口和XML文件映射不上。检查三点Mapper接口加了Mapper注解或在启动类加了MapperScanXML文件在resources/mapper目录下且路径和接口包名对应mybatis-plus.mapper-locations配置正确。端口被占用Port 8080 was already in use。经验做法不是随机换端口而是找出占用进程杀掉# Windows netstat -ano | findstr 8080 taskkill /pid [PID] /f # Linux/macOS lsof -i :8080 kill -9 [PID]6.2 Vue3前端高频报错vue-tsc类型报错入了TypeScript坑最容易报的是Property xxx does not exist on type never。通常是因为ref([])没有指定泛型正确写法是refReceiptVO[]([])。这类报错在npm run build时才会触发因为vue-tsc会做类型检查开发时可能没发现上线构建时突然挂掉挺闹心的。建议在package.json的build脚本中把vue-tsc去掉Vite本身就能完成打包类型检查交给IDE的编辑器提示即可。这一步能让你的构建速度提升一倍以上。Element Plus的el-table列宽不生效表格列设置width无效多半是table父容器没设宽度或者autoWidth与固定列混用导致的。单独一列加fixedright记得同时给操作列设width否则“编辑”“删除”按钮会挤压在一行里换行。Axios请求报400前端请求参数和后端RequestBody接收的JSON结构对不上。最常见的坑后端接收的是ListLong前端传了[1,2,3]没问题但如果后端要Map前端传了数组会报JSON parse error。排查方式打开浏览器DevTools的Network看请求Payload的原始JSON对照后端DTO字段。6.3 Android端联调高频报错CLEARTEXT communication not permittedAndroid 9API 28起默认禁止明文HTTP流量。开发阶段后端用HTTP就必须要让App允许明文流量新建res/xml/network_security_config.xmlnetwork-security-config base-config cleartextTrafficPermittedtrue / /network-security-config在AndroidManifest.xml的application节点上设置android:networkSecurityConfigxml/network_security_config。或者直接在application节点加android:usesCleartextTraffictrue。后者粗暴但开发时最省事。FileUriExposedException原因就是前面提到的Android 7.0以后不能直接用file://URI打开相机。解决用FileProvider路径配置时注意paths external-path nameexternal_files path. / /pathspath.表示整个外部存储目录都授权开发时省心上线前可以收紧范围。RecyclerView列表滑动卡顿通常不是性能问题而是布局嵌套问题。检查Item布局是否有不必要的嵌套层级比如LinearLayout套LinearLayout再套LinearLayout尽量用ConstraintLayout。另外不要在onBindViewHolder里做耗时操作比如解析JSON、获取当前时间格式化这些可以提前算好缓存。6.4 业务流程中的坑对账不平怎么办上线跑了两周财务说对账不平——实收金额和账单已缴金额对不上。排查后发现是这样一个场景用户发起缴费生成了支付订单但用户没有完成支付就关掉了页面。此时账单还是“未缴”状态但订单表里多了一条“待支付”的记录。财务导出的“实收金额”是按已支付订单统计的却把待支付订单也算进去了。解决对账报表只统计status SUCCESS的支付订单。另外加一个定时任务把超过30分钟未支付的订单标记为“已关闭”并释放对账单的锁定状态。这样一个用户如果重复发起缴费就不会因为“上一笔待支付订单”的原因被锁死。6.5 部署上线注意从开发环境到服务器本地跑通不代表部署没问题。我一般走这样的流程先装环境服务器上装JDK用openjdk-17或openjdk-8对应Spring Boot版本、MySQL、Redis、Nginx。然后后端打jar包mvn clean package -DskipTests上传到服务器后用nohup启动nohup java -jar water-server.jar --spring.profiles.activeprod app.log 21 配置分离application-prod.yml里的数据库密码、Redis密码用环境变量注入不要把生产密码写进代码实仓。前端打包npm run build把dist目录传到Nginx的html目录Nginx配置反向代理location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意前端请求接口的baseURL在开发时要区分环境。我建了.env.development和.env.production文件# .env.development VITE_API_BASE_URL/apiVite的proxy配置在开发环境代理到http://localhost:8080生产环境由Nginx把/api反向代理到后端。这样代码里不需要写死IP打包一次发给谁都一样跑。Android端打包APK前把BuildConfig.API_BASE_URL设置成生产环境地址公网IP或域名。如果你没有域名和备案就用IP地址但千万别用内网PTIP——用户装的是正式包拿到的地址得是公网可达的。7. 个人总结与经验心得这个项目从头到尾做完我个人最有感触的一点是技术栈只是外壳业务才是灵魂。Spring Boot Vue3 Android这三件套放在任何一个管理系统里都能跑但这个系统之所以叫“水务缴费系统”是因为每一步都要贴合真实的水务业务。从抄表员的户外任务、到阶梯水价的计算、再到“估抄”和“实测”的差异处理每一个细节都是需求分析阶段从真实场景里抽出来的。如果只把它当CRUD来做做完只是一个“能增删改查的网站”而不是“能用的系统”。第二点体会是三端联调一定要提前约定接口规范。我在项目第一天就写了一份接口文档哪怕是粗略版的统一了日期格式、分页参数、状态码、错误信息结构。否则后端返回2025-03-01 12:00:00Android端认为这是字符串Vue3端认为这是时间对象处理逻辑完全不一样联调时候全在扯皮。最后一点多端项目最容易被忽视的是“异常路径”。正常流程大家都写得出来但像“支付超时”“Token过期后重试”“抄表员上传照片失败”“对账不平”这些异常路径才是真正决定系统上线后能否活下去的关键。我建议在做工时估算时别只算正常页面和正常接口的工作量至少留出20%的buffer给异常处理和联调。这是很多新手低估的部分也是项目延期的最主要原因。如果后面还有机会做二期我可能会给这个项目加一个“智能水表对接”的MQTT模块让设备端直接上报读数彻底替代人工抄表。但那是另一个故事了——先把这套传统抄表流程跑通、跑稳、跑出信任感比什么都重要。
返回列表