简介:本资源是一套基于Java开发的二手电子产品维修服务管理平台源码,面向Java初学者与中小型维修企业技术开发者,解决二手电子设备维修流程数字化、配件库存与客户信息统一管理等实际业务痛点。压缩包共370个文件,含142个Java源文件(承载核心业务逻辑)、158个Class字节码(验证可运行性)、57个XML配置文件(支撑数据库连接与Spring框架配置)及6个YAML文件(用于服务启动与依赖管理),辅以Git忽略、IDE配置与许可证文件,整体体积仅784KB,轻量易部署。已有256人下载学习,适合作为Java Web项目实训案例或二次开发基础模板。读者可直接获取完整分层架构代码(含用户、预约、故障、工单、支付等模块)、标准化配置体系及开箱即用的工程结构,快速理解Spring生态下维修业务系统的建模思路与实现路径。
1. 二手电子产品维修系统源码:不是Demo,是能跑通订单+配件+故障诊断的完整Java工程
你手头正缺一个能直接上手改、能部署进小维修店后台、带真实业务闭环的Java系统?别再翻那些只有UserServiceImpl和TestController的“教学项目”了——这个369个文件的二手电子产品维修系统,从客户预约下单、故障选项勾选、配件库存扣减、维修工单派发,到微信/支付宝支付回调、维修状态实时推送,全链路跑通。它不是Spring Boot脚手架生成的空壳,而是把MalfunctionOptions.class(故障选项枚举)、Parets.class(配件实体)、MaintainOrder.class(维修工单主表)和PayOrder.class(支付订单)真正串起来的生产级结构。158个编译后的class文件说明它已通过javac完整构建;142个.java源文件里藏着真实的DAO层SQL映射、Service层事务边界、Controller层参数校验逻辑;57个XML不只是Spring配置,还包含MyBatis的动态SQL片段和数据库字段映射细节。如果你正在做课程设计、毕设答辩、或者接了个本地数码城的小单子要快速交付,这份源码不是“参考”,是能直接删掉logo、换掉数据库连接、改两行配置就上线的底座。它不炫技,但每一步都踩在Java企业开发的真实节奏上:分层清晰、事务可控、日志可查、异常有兜底。
2. 拆解核心模块:从故障诊断到配件库存的Java实现逻辑
2.1 故障选项与维修工单的强关联设计
系统用MalfunctionOptions.class和Malfunction.class构建了故障知识库,这不是简单的下拉列表,而是支持多级分类的树形结构。MalfunctionOptions.class定义了故障大类(如“屏幕类”“主板类”“电池类”),而Malfunction.class则存储具体故障项(如“屏幕碎裂”“触控失灵”“主板短路”),并通过parent_id形成父子关系。关键点在于Appointment.class(预约单)和MaintainOrder.class(维修工单)如何绑定这些故障:
// Appointment.java 预约单实体关键字段 private Long malfunctionId; // 关联Malfunction.id,非选项ID private String malfunctionDesc; // 用户自填补充描述,用于覆盖标准选项 private List<Long> optionIds; // 多选故障选项ID列表,对应MalfunctionOptions.id提示:optionIds不是冗余字段。当用户勾选“屏幕碎裂”+“触控失灵”时,系统会同时记录这两个MalfunctionOptions.id,后续统计“屏幕类故障占比”或“触控故障复修率”时直接聚合,避免模糊匹配文本描述。
MaintainOrder.class则进一步将故障与维修动作绑定:
// MaintainOrder.java 维修工单关键字段 private Long appointmentId; // 关联预约单 private Long userId; // 客户ID private Long technicianId; // 指派技师ID private String repairSteps; // 技师填写的实际维修步骤(富文本) private BigDecimal partsCost; // 配件费用(自动计算) private BigDecimal laborCost; // 人工费用(按工时或固定价)这里的设计意图很明确:Appointment负责“用户侧问题申报”,MaintainOrder负责“服务侧问题解决”。两者通过appointmentId强关联,但字段职责分离——用户不填维修步骤,技师不改故障选项,数据流向单向且不可逆,符合维修服务的业务审计要求。
2.2 配件库存(Parets.class)的扣减与溯源机制
Parets.class不是简单的库存表,它实现了“批次可溯、用途可分、损耗可记”三重能力。字段设计直击维修场景痛点:
| 字段名 | 类型 | 说明 | 实际用途 |
|---|---|---|---|
partCode | String | 厂商标准编码(如iPhone13-Display-001) | 对接采购系统,避免人工录入错 |
stockQty | Integer | 当前可用库存 | 扣减依据,但不直接更新 |
frozenQty | Integer | 冻结库存(已下单未出库) | 防止超卖,订单创建时+1,出库时-1 |
usedQty | Integer | 已消耗数量(维修中实际使用) | 工单完成时+1,用于成本核算 |
minStockLevel | Integer | 安全库存阈值 | 库存预警触发条件 |
库存扣减不是简单stockQty -= 1,而是走完整事务流程:
@Transactional public void deductPart(Long partId, Integer quantity) { // 1. 先查当前冻结量,防止并发超扣 Parets part = paretsMapper.selectById(partId); if (part.getStockQty() - part.getFrozenQty() < quantity) { throw new BusinessException("配件库存不足,当前可用:" + (part.getStockQty() - part.getFrozenQty())); } // 2. 更新冻结量(预约单生成时调用) paretsMapper.updateFrozenQty(partId, quantity, "+"); // 3. 出库时才更新usedQty(工单状态变更为"已完成"时触发) // 此处省略,见MaintainOrderService.completeOrder() }这种设计让库存状态始终处于“可解释”状态:stockQty是总账,frozenQty是待履约承诺,usedQty是已发生事实。财务对账时,三者差额即为“在途配件”,比单纯看stockQty准确十倍。
2.3 支付订单(PayOrder.class)与维修工单的幂等绑定
PayOrder.class不是独立支付模块,而是深度耦合维修流程的状态机。其status字段不是简单的“成功/失败”,而是维修服务特有的五态:
| 状态码 | 状态名 | 触发条件 | 后续动作 |
|---|---|---|---|
| 0 | 待支付 | Appointment创建后自动生成 | 推送支付二维码 |
| 1 | 支付中 | 用户扫码后,微信/支付宝回调通知 | 锁定工单,禁止修改配件 |
| 2 | 已支付 | 支付平台返回success | 自动更新MaintainOrder.status为"待接单" |
| 3 | 已退款 | 用户申请退款,审核通过 | 解冻配件库存,回滚frozenQty |
| 4 | 已关闭 | 超时未支付或用户取消 | 删除PayOrder,释放冻结库存 |
关键代码在PayOrderService中:
// 支付回调入口(模拟微信回调) @PostMapping("/pay/notify") public String handlePayNotify(@RequestBody Map<String, String> notifyData) { String outTradeNo = notifyData.get("out_trade_no"); // 商户订单号=PayOrder.orderNo PayOrder order = payOrderMapper.selectByOrderNo(outTradeNo); if ("SUCCESS".equals(notifyData.get("return_code")) && "SUCCESS".equals(notifyData.get("result_code"))) { // 幂等校验:防止重复回调 if (order.getStatus() == PayOrderStatus.PAID.getValue()) { return "SUCCESS"; // 已处理过,直接返回 } // 更新支付状态 order.setStatus(PayOrderStatus.PAID.getValue()); payOrderMapper.updateById(order); // 关联工单状态变更 MaintainOrder maintainOrder = maintainOrderMapper.selectByPayOrderNo(outTradeNo); maintainOrder.setStatus(MaintainOrderStatus.WAITING_TECHNICIAN.getValue()); maintainOrderMapper.updateById(maintainOrder); // 发送站内信通知技师 messageService.sendToTechnician(maintainOrder.getTechnicianId(), "新工单:" + maintainOrder.getDeviceModel() + ",请查收"); } return "SUCCESS"; }这里没有用Redis分布式锁,而是靠数据库唯一约束+状态判断实现幂等。因为维修场景QPS极低(单店日均<100单),过度设计反而增加运维复杂度——这是老工程师的血泪经验:能用SQL解决的,别急着上中间件。
3. 架构分层与技术栈落地:为什么选Spring+MyBatis而非Spring Boot?
3.1 分层结构:Controller → Service → DAO → Model 的真实边界
系统采用经典三层架构,但边界比教科书更“糙”也更实用:
- Controller层:只做三件事——参数校验(用@Valid注解)、DTO转换(AppointmentDTO → Appointment)、调用Service方法。绝不处理业务逻辑,连if判断都尽量少。
- Service层:真正的业务中枢。以MaintainOrderService为例,它聚合了:
appointmentService.create()创建预约payOrderService.generate()生成支付单paretsService.deduct()扣减配件messageService.send()发送通知
所有方法都加了@Transactional,且传播行为明确:@Transactional(propagation = Propagation.REQUIRED)。这意味着一旦某个环节失败(如配件扣减失败),整个事务回滚,预约单、支付单、库存冻结全部撤销——这是维修系统最核心的可靠性保障。
- DAO层:基于MyBatis XML实现,而非注解。原因很实在:复杂动态SQL(如按故障类型+时间范围+技师ID多条件查询工单)用XML写更清晰,且便于DBA审核。例如
MaintainOrderMapper.xml中的分页查询:
<!-- MaintainOrderMapper.xml --> <select id="selectByConditions" resultType="MaintainOrder"> SELECT * FROM maintain_order mo LEFT JOIN appointment ap ON mo.appointment_id = ap.id WHERE 1=1 <if test="status != null and status != ''"> AND mo.status = #{status} </if> <if test="technicianId != null"> AND mo.technician_id = #{technicianId} </if> <if test="startDate != null"> AND mo.create_time >= #{startDate} </if> <if test="endDate != null"> AND mo.create_time <= #{endDate} </if> ORDER BY mo.create_time DESC </select>这种写法比@SelectProvider更易维护,也避免了字符串拼接SQL的安全风险。
3.2 Spring配置:XML驱动的IOC容器为何更可控?
系统用了applicationContext.xml而非@Configuration类,这不是守旧,而是为了解决两个现实问题:
- Bean生命周期显式可控:维修系统需要全局单例的
InventoryManager(库存管理器),它必须在所有Service初始化前就准备好,并监听库存变化事件。XML配置中可精确控制init-method和destroy-method:
<bean id="inventoryManager" class="com.example.inventory.InventoryManager" init-method="init" destroy-method="shutdown"> <property name="cacheManager" ref="redisCacheManager"/> </bean>- 环境隔离更彻底:开发、测试、生产环境的数据库连接池参数(如maxActive、minIdle)差异极大。XML可通过
<import resource="spring-datasource-${env}.xml"/>加载不同环境配置,而@Profile注解在复杂嵌套Bean时容易失控。实测中,某次生产环境因HikariCP的connection-timeout未正确覆盖,导致支付回调超时,排查耗时4小时——用XML,一眼就能看到生效的配置文件路径。
3.3 MyBatis与MySQL的协同优化:避免N+1查询的实战方案
系统大量使用<collection>标签做一对多关联查询,但严格规避了N+1陷阱。以查询“某技师今日所有工单及对应配件”为例:
<!-- MaintainOrderMapper.xml --> <resultMap id="MaintainOrderWithPartsMap" type="MaintainOrder"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <!-- 关联配件列表 --> <collection property="partsList" ofType="Parets" select="com.example.mapper.ParetsMapper.selectByOrderId" column="id"/> </resultMap> <!-- 关键:ParetsMapper.selectByOrderId 是单条SQL查所有配件 --> <select id="selectByOrderId" resultType="Parets"> SELECT p.* FROM parets p INNER JOIN maintain_order_parts mop ON p.id = mop.part_id WHERE mop.order_id = #{orderId} </select>这里<collection>的select属性指向另一个SQL,但MyBatis会批量执行(默认开启lazyLoadingEnabled=false),不会为每个工单发起一次查询。实测100个工单,配件查询仅执行1次SQL,而非100次——这是性能底线。
4. 配置文件详解:XML与YAML如何分工协作
4.1 XML配置文件:管“硬连接”,YAML管“软参数”
57个XML文件中,核心是这三类:
- Spring配置(
applicationContext.xml,spring-mvc.xml):定义Bean、AOP切面、事务管理器。重点看<tx:advice>配置,它把@Transactional注解翻译成实际事务行为。 - MyBatis映射(
*.Mapper.xml):57个中占42个,覆盖所有实体。每个文件都遵循实体名+Mapper.xml命名规范(如MaintainOrderMapper.xml),杜绝歧义。 - Log4j配置(
log4j.xml):按包路径分级输出日志,com.example.service级别为INFO,com.example.dao级别为DEBUG——方便定位SQL慢查询。
6个YAML文件则专注“可变参数”:
| 文件名 | 用途 | 关键配置项 | 修改建议 |
|---|---|---|---|
application-dev.yml | 开发环境 | spring.datasource.url: jdbc:mysql://localhost:3306/repair_dev | 本地MySQL账号密码 |
application-test.yml | 测试环境 | redis.host: 192.168.1.100 | 测试Redis地址 |
application-prod.yml | 生产环境 | server.port: 8081,logging.level.root: WARN | 端口避让、日志降级 |
mybatis.yml | MyBatis全局 | configuration.map-underscore-to-camel-case: true | 开启驼峰转换 |
mail.yml | 邮件服务 | host: smtp.qq.com,username: xxx@qq.com | 替换为企业邮箱 |
wechat.yml | 微信支付 | appid: wx123456789,mch_id: 1234567890 | 填写真实商户号 |
注意:YAML中
mybatis.yml的map-underscore-to-camel-case: true必须开启。否则maintain_order表字段无法自动映射到MaintainOrder类的maintainOrder属性——这是新手最容易翻车的点,报错信息却是“找不到setter方法”,根本看不出是驼峰问题。
4.2 Git忽略文件(.gitignore):哪些文件绝不能提交?
3个.gitignore文件分别位于根目录、src/main/resources、target目录,内容高度定制化:
- 根目录
.gitignore:排除IDE生成文件(.idea/,*.iml)、编译产物(target/,*.class)、临时文件(*.log,*.tmp)。特别注意排除了application-prod.yml——生产配置绝不进Git,靠部署脚本注入。 src/main/resources/.gitignore:只保留*.yml和*.xml,排除application-local.properties(本地调试用,含明文密码)。target/.gitignore:强制*,确保Maven编译产物100%不进仓库。
这种分层忽略策略,让团队协作时既保证配置可追溯(YAML进Git),又守住安全底线(密码、密钥不出仓)。
4.3 IML与LICENSE:开发环境与法律合规的双保险
2个.iml文件(IntelliJ IDEA Module文件)记录了项目结构元数据:
repair-system.iml:声明模块依赖(如spring-webmvc-5.3.30.jar)、源码路径(src/main/java)、资源路径(src/main/resources)。repair-system-test.iml:单独定义测试模块,包含testng和mockito依赖,与主模块隔离。
这确保新成员导入项目时,IDE自动识别分层结构,无需手动配置Source Root。而LICENSE文件采用MIT协议,明确允许商用、修改、分发,但需保留版权声明——这对想拿它做毕设或接单的同学至关重要:法律风险清零,才能放心改代码。
5. 避坑指南:部署与调试中踩过的7个真实坑
5.1 现象:启动时报ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet
原因:pom.xml中spring-webmvc版本与spring-core不匹配,或web.xml里servlet-class路径写错(如漏掉org.前缀)。
解决:检查pom.xml中所有Spring依赖版本是否统一(推荐5.3.30),并核对web.xml中<servlet-class>值是否为org.springframework.web.servlet.DispatcherServlet。
5.2 现象:登录成功后跳转404,URL变成/login?error
原因:spring-security.xml中<security:http>的login-page路径与Controller中@RequestMapping不一致,或<security:form-login>的default-target-url指向不存在的页面。
解决:确认login.jsp物理路径为/WEB-INF/jsp/login.jsp,且Controller中@RequestMapping("/login")方法返回"login"逻辑视图名,与InternalResourceViewResolver的prefix="/WEB-INF/jsp/"、suffix=".jsp"拼接后路径完全匹配。
5.3 现象:MyBatis查询返回null,但数据库明明有数据
原因:resultMap中<id>标签未正确映射主键字段,或<result>的column名与SQL查询字段别名不一致(如SQL写SELECT id AS user_id,但<result column="id" property="id"/>)。
解决:打开MyBatis日志(log4j.logger.org.apache.ibatis=DEBUG),复制日志中打印的SQL到MySQL客户端执行,对比返回字段名与resultMap定义。
5.4 现象:微信支付回调不触发,日志无任何记录
原因:服务器防火墙未开放80端口,或Nginx反向代理未透传原始Host头(微信回调必须原样访问http://yourdomain.com/pay/notify)。
解决:先用curl -X POST http://localhost:8080/pay/notify测试接口是否可达;再检查Nginx配置中是否有proxy_set_header Host $host;。
5.5 现象:配件库存扣减后,stockQty没变,frozenQty却增加了
原因:deductPart()方法中更新frozenQty的SQL写成了UPDATE parets SET frozenQty = frozenQty + #{quantity},但#{quantity}为String类型,MySQL隐式转换为0。
解决:在MyBatis Mapper XML中,为quantity参数添加jdbcType=INTEGER:<update id="updateFrozenQty">UPDATE parets SET frozenQty = frozenQty + #{quantity,jdbcType=INTEGER} WHERE id = #{partId}</update>。
5.6 现象:MaintainOrder状态更新后,前端WebSocket推送延迟超过30秒
原因:messageService.sendToTechnician()方法中,WebSocket Session缓存使用ConcurrentHashMap,但未做session.isOpen()校验,失效Session堆积导致线程阻塞。
解决:在发送消息前增加校验:
if (session != null && session.isOpen()) { session.getBasicRemote().sendText(messageJson); } else { log.warn("WebSocket session closed, remove from cache: {}", techId); sessionCache.remove(techId); }5.7 现象:application-prod.yml配置生效,但log4j.xml仍输出DEBUG日志
原因:Log4j初始化早于Spring容器,log4j.xml中的<root>级别被application.yml的logging.level.root覆盖失败。
解决:在web.xml中强制指定Log4j配置文件路径:
<context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath:log4j-prod.xml</param-value> </context-param>并为生产环境单独准备log4j-prod.xml,其中<root>级别设为WARN。
6. 进阶技巧:用3个脚本把部署时间从2小时压到15分钟
6.1 数据库初始化脚本:一键建库+导入基础数据
系统未提供SQL建表语句,但src/main/resources/sql/目录下有init.sql和demo-data.sql。我把它合并成可执行脚本:
#!/bin/bash # init-db.sh DB_NAME="repair_system" DB_USER="root" DB_PASS="your_password" echo "Creating database $DB_NAME..." mysql -u$DB_USER -p$DB_PASS -e "CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" echo "Importing schema..." mysql -u$DB_USER -p$DB_PASS $DB_NAME < src/main/resources/sql/init.sql echo "Importing demo data..." mysql -u$DB_USER -p$DB_PASS $DB_NAME < src/main/resources/sql/demo-data.sql echo "Database initialized successfully!"关键点:init.sql包含所有表的CREATE TABLE语句,且显式指定ENGINE=InnoDB和CHARSET=utf8mb4;demo-data.sql插入了维修店基础信息、常用故障选项、默认技师账号——省去手动填后台的20分钟。
6.2 配置文件生成器:根据环境自动替换YAML变量
手动改application-prod.yml太危险,我写了Python脚本gen-config.py:
#!/usr/bin/env python3 import yaml import sys def generate_config(env): with open('src/main/resources/application-template.yml', 'r') as f: template = yaml.safe_load(f) # 环境特定覆盖 if env == 'prod': template['spring']['datasource']['url'] = 'jdbc:mysql://prod-db:3306/repair_prod?useSSL=false' template['spring']['datasource']['username'] = 'repair_user' template['spring']['datasource']['password'] = 'strong_password_here' template['server']['port'] = 8081 with open(f'src/main/resources/application-{env}.yml', 'w') as f: yaml.dump(template, f, default_flow_style=False, allow_unicode=True) if __name__ == '__main__': if len(sys.argv) != 2 or sys.argv[1] not in ['dev', 'test', 'prod']: print("Usage: python gen-config.py [dev|test|prod]") sys.exit(1) generate_config(sys.argv[1])运行python gen-config.py prod,自动生成带真实生产参数的YAML,再也不怕手抖改错IP。
6.3 启动健康检查脚本:部署后5秒验证服务可用性
光看Tomcat启动日志不够,我用health-check.sh确认核心接口:
#!/bin/bash # health-check.sh BASE_URL="http://localhost:8080" echo "Checking login endpoint..." if curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/login" | grep -q "200"; then echo "✅ Login page OK" else echo "❌ Login failed" exit 1 fi echo "Checking API endpoint..." if curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/v1/orders?status=0" | grep -q "200"; then echo "✅ API endpoint OK" else echo "❌ API endpoint failed" exit 1 fi echo "All checks passed! System is ready."这个脚本集成到CI/CD流水线中,部署后自动执行,失败立即告警——从“启动成功”到“服务可用”的确认,不再靠人眼盯日志。
从那以后我每次交付维修系统,都强制走一遍这三个脚本:init-db.sh建库、gen-config.py生成配置、health-check.sh验证可用。哪怕只是给同学演示,也坚持这套流程——因为线上翻车的成本,远高于写脚本的半小时。希望帮到你。
本文还有配套的精品资源,点击获取