news 2026/10/29 19:55:55

Java维修系统实战:Spring Boot+MyBatis-Plus构建高并发、低配置维修业务系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java维修系统实战:Spring Boot+MyBatis-Plus构建高并发、低配置维修业务系统

简介:本资源是一套面向Java初学者与中级开发者的学习型项目源码,聚焦二手电子产品维修服务全流程管理,适用于课程设计、毕业设计或中小维修企业信息化系统原型开发。压缩包共370个文件,总计784KB,涵盖142个Java源文件(承载核心业务逻辑)、158个Class字节码(含Users、MaintainOrder、PayOrder等关键实体与服务类)、57个XML配置文件(用于数据库连接与Spring框架配置)及6个YAML文件(支持环境化部署),辅以Git忽略、IDE配置与LICENSE等工程必需文件。已有256人学习下载,体现其在教学实践与轻量级系统开发中的实用价值。读者可直接导入IDE运行调试,完整掌握基于Spring生态的分层架构设计、维修工单流转、客户与配件管理模块实现,以及XML/YAML双配置模式下的系统集成方法,具备良好的可扩展性与二次开发基础。

1. 为什么一个“二手电子产品维修系统”非得用 Java 写?——不是为了炫技,而是修手机、换屏幕、查配件、开单、算账、管库存,全得扛住每天 200+ 工单的并发压测和三年不重启的稳定运行

你可能在招聘网站上见过这类 JD:“熟悉 Spring Boot,有维修管理类系统开发经验者优先”;也可能在本地数码城二楼看到过贴着“苹果/华为/小米专修”的小门脸,墙上挂着几台拆到一半的 iPhone,柜台玻璃下压着一叠手写维修单。这两者之间,缺的不是技术,而是一套真正能跑在修理工手指头上的 Java 系统:它要能扫码识别主板型号,自动匹配兼容配件;要能在师傅一边拧螺丝一边用安卓平板点“已更换电池”,后端立刻扣减库存、生成电子工单、触发微信通知客户;还要支持老板月底导出“屏幕更换TOP5机型”“平均返修率超15%的批次”这类报表。这不是 ERP 的简化版,也不是 OA 的变体——它是嵌在维修动作流里的业务操作系统。Java 在这里不是因为“企业级”三个字好听,而是因为它的线程模型能稳扛多终端录入(前台接单+后台仓管+师傅Pad),JDBC 生态让 MySQL/Oracle 切换零成本,Spring Security 能给店长、技师、仓管配三套细粒度权限,更重要的是,全国 73% 的中小维修连锁用的还是 Windows 7/10 旧电脑,而 Java SE 8+ 的 JRE 仍能原生兼容——你没法指望 Python 的 pyinstaller 打包包在一台连蓝牙都打不开的老联想启天M430上顺利解压运行。本文就带你从零搭起这个系统:不讲 MVC 是什么,只告诉你 Controller 层怎么防住师傅误点“重复提交”、MyBatis 的 resultMap 怎么映射“维修项-子配件-历史记录”三级嵌套、库存扣减为什么必须加SELECT ... FOR UPDATE而不是简单 update,以及——最实在的——源码里哪 3 个文件改完就能直接部署到修车行隔壁那台尘封已久的 Dell OptiPlex 3020 上跑起来。

2. 用 Spring Boot 2.7 + MyBatis-Plus 快速搭建维修核心模块:从数据库建模到工单状态机落地

2.1 数据库设计:避开“一张大表全搞定”的玄学陷阱,按维修生命周期切分四张主表

维修系统最典型的翻车现场,就是把“客户信息、设备信息、故障描述、配件清单、维修步骤、付款状态”全塞进一张repair_order表里。结果半年后老板要查“近三个月更换过原装电池但未做防水检测的华为机型”,SQL 直接卡死。我们按真实维修动线拆成四张表,字段精简到可维护:

表名核心字段(非全部)设计要点
customerid,name,phone,wechat_id,created_atphone加唯一索引,避免同一客户多次录入;wechat_id存微信 openid,用于后续消息推送
deviceid,sn,brand,model,imei,customer_id,status(0=待检,1=在修,2=已取)sn和imei均设唯一索引,支持扫码枪快速定位;status用 tinyint 而非字符串,减少索引体积
repair_orderid,device_id,submit_time,expected_finish,actual_finish,status(0=待接单,1=已派工,2=维修中,3=待质检,4=已完成,5=已取消)status字段是状态机核心,所有业务操作必须通过状态流转校验(如:只有 status=1 才能派工)
repair_itemid,order_id,item_type(1=人工费,2=配件费,3=检测费),item_name,price,quantity,part_code(配件编码)一张订单可含多条维修项,支持“换屏+贴膜+清灰”组合计价;part_code关联配件库,为库存扣减埋点

提示:repair_order.status不要依赖前端传值!所有状态变更必须走 Service 层的updateStatus(Long orderId, Integer fromStatus, Integer toStatus)方法,内部先SELECT ... WHERE id = ? AND status = ?校验当前状态,再UPDATE ... SET status = ? WHERE id = ? AND status = ?——这是防止并发修改导致状态错乱的后悔药。

2.2 Spring Boot 启动脚本与关键配置:让系统在老式办公机上不爆内存

很多维修店的电脑还是 4GB 内存 + 机械硬盘,Spring Boot 默认启动参数会直接 OOM。我们在src/main/resources/application.yml中强制约束 JVM:

spring: datasource: url: jdbc:mysql://localhost:3306/repair_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: repair_user password: repair_pass_2024 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none # 绝对禁止 auto-create!生产环境必须手动建表 show-sql: false properties: hibernate: format_sql: false # 关键:降低内存占用 server: port: 8081 tomcat: max-connections: 200 # 老机器扛不住 1000 连接 accept-count: 100 # 队列长度压缩到 100 # MyBatis-Plus 配置 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期看 SQL,上线前注释掉 global-config: db-config: id-type: assign_id # 使用雪花算法生成 Long ID,避免 UUID 占空间

对应pom.xml中锁定轻量级依赖(删掉所有spring-boot-starter-websocket、spring-boot-starter-cache等非必需模块):

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 内嵌 Undertow 替代 Tomcat,内存占用低 30% --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 锁定 3.5.x,兼容 JDK 8 --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

2.3 维修工单状态机实现:用枚举 + 状态流转校验,堵死“跳步”漏洞

维修流程不是线性链条,而是带条件分支的状态图:比如“已派工”后可进入“维修中”,但若客户临时加价,则需退回“待接单”重新计价。我们用枚举定义合法流转:

// src/main/java/com/repair/enums/OrderStatusEnum.java public enum OrderStatusEnum { WAITING_ACCEPT(0, "待接单", Collections.singletonList(1)), // 只能流向 1(已派工) ASSIGNED(1, "已派工", Arrays.asList(2, 0)), // 可流向 2(维修中)或 0(退回) REPAIRING(2, "维修中", Arrays.asList(3, 5)), // 可流向 3(待质检)或 5(取消) QUALITY_CHECK(3, "待质检", Arrays.asList(4, 2)), // 可流向 4(完成)或 2(返工) COMPLETED(4, "已完成", Collections.emptyList()), CANCELLED(5, "已取消", Collections.emptyList()); private final int code; private final String desc; private final List<Integer> allowedNext; // 允许流向的状态码列表 OrderStatusEnum(int code, String desc, List<Integer> allowedNext) { this.code = code; this.desc = desc; this.allowedNext = allowedNext; } public boolean canTransferTo(int targetCode) { return this.allowedNext.contains(targetCode); } // 根据 code 获取枚举实例 public static OrderStatusEnum fromCode(int code) { for (OrderStatusEnum e : values()) { if (e.code == code) return e; } throw new IllegalArgumentException("Invalid order status code: " + code); } }

Service 层状态更新方法强制校验:

// src/main/java/com/repair/service/impl/RepairOrderServiceImpl.java @Service public class RepairOrderServiceImpl implements RepairOrderService { @Override @Transactional public boolean updateStatus(Long orderId, Integer targetStatus) { RepairOrder order = orderMapper.selectById(orderId); if (order == null) { throw new RuntimeException("工单不存在: " + orderId); } OrderStatusEnum current = OrderStatusEnum.fromCode(order.getStatus()); if (!current.canTransferTo(targetStatus)) { throw new RuntimeException( String.format("状态非法流转:从 %s(%d) 到 %s(%d)", current.getDesc(), current.getCode(), OrderStatusEnum.fromCode(targetStatus).getDesc(), targetStatus) ); } // 加锁更新,防止并发覆盖 int updated = orderMapper.update( new UpdateWrapper<RepairOrder>() .eq("id", orderId) .eq("status", current.getCode()) // 确保当前状态未被其他线程修改 .set("status", targetStatus) .set("updated_at", LocalDateTime.now()) ); return updated > 0; } }

注意:updateStatus方法中UpdateWrapper的.eq("status", current.getCode())是关键。它让 SQL 变成UPDATE repair_order SET status=?, updated_at=? WHERE id=? AND status=?,如果此时另一线程已将状态改为 2,本次更新将返回 0 行影响,事务回滚——比在代码里if (order.getStatus() != current.getCode())多一层数据库级防护。

3. 配件库存精准扣减:为什么不能用“update set stock = stock - 1”,而必须用 SELECT FOR UPDATE

3.1 并发场景下的库存超卖血泪现场

想象维修师傅 A 和 B 同时接到两台 iPhone 12 更换屏幕的单子。库存表里screen_iphone12的stock当前为 1。两人几乎同时点击“确认更换”,后端执行:

UPDATE part_stock SET stock = stock - 1 WHERE part_code = 'screen_iphone12';

结果:两条 SQL 都成功执行,stock变成 -1。第二天客户来取机,发现屏幕没货,只能道歉补发——这就是典型的“超卖”。根本原因在于:UPDATE ... SET stock = stock - 1是一个读-改-写操作,但 MySQL 默认隔离级别(REPEATABLE READ)下,它不锁住被修改的行,直到语句结束才加锁,中间存在时间窗口。

3.2 正确解法:SELECT ... FOR UPDATE + 事务内原子扣减

我们必须让“读库存”和“扣库存”成为不可分割的原子操作。MyBatis-Plus 本身不支持SELECT ... FOR UPDATE,需手写 XML:

<!-- src/main/resources/mapper/PartStockMapper.xml --> <mapper namespace="com.repair.mapper.PartStockMapper"> <select id="selectForUpdateByCode" resultType="com.repair.entity.PartStock"> SELECT id, part_code, stock, updated_at FROM part_stock WHERE part_code = #{partCode} FOR UPDATE </select> <update id="decreaseStock"> UPDATE part_stock SET stock = stock - #{quantity}, updated_at = NOW() WHERE part_code = #{partCode} AND stock >= #{quantity} </update> </mapper>

对应的 Service 方法:

@Service public class PartStockService { @Transactional public boolean decreaseStock(String partCode, Integer quantity) { // 1. 加锁查询当前库存 PartStock stock = partStockMapper.selectForUpdateByCode(partCode); if (stock == null) { throw new RuntimeException("配件不存在: " + partCode); } if (stock.getStock() < quantity) { throw new RuntimeException( String.format("库存不足:配件 %s 需求 %d,当前仅剩 %d", partCode, quantity, stock.getStock()) ); } // 2. 执行扣减(WHERE 条件确保不会扣成负数) int updated = partStockMapper.decreaseStock(partCode, quantity); if (updated == 0) { // 理论上不会走到这里,因前面已校验 stock >= quantity throw new RuntimeException("库存扣减失败,可能已被其他事务修改"); } return true; } }

提示:SELECT ... FOR UPDATE在事务中执行,会锁住part_code对应的行(即使part_code有索引)。这意味着当师傅 A 在扣screen_iphone12库存时,师傅 B 的相同查询会被阻塞,直到 A 的事务提交或回滚。这是牺牲一点响应时间,换取数据绝对一致的必要代价。

3.3 配件与维修项的强绑定:用 JSON 字段存“更换记录”,而非硬建关联表

维修系统有个隐藏需求:每次更换配件,都要记录“谁换的、什么时候换的、旧件去向(报废/返厂)、新件序列号”。如果为每条更换记录建repair_part_log表,字段爆炸且查询复杂。更务实的做法是——在repair_item表中增加log_jsonTEXT 字段,存标准化 JSON:

[ { "operator": "张师傅", "time": "2024-06-15T14:22:33", "old_sn": "SN20240615001", "new_sn": "SN20240615002", "action": "更换屏幕", "dispose": "报废" } ]

Java 实体类中用@TableField(typeHandler = JacksonTypeHandler.class)映射:

public class RepairItem extends Model<RepairItem> { // ... 其他字段 @TableField(typeHandler = JacksonTypeHandler.class) private List<PartLog> logJson; // 自定义 PartLog 类,含 operator/time 等字段 }

这样做的好处:

  • 查询工单详情时,repair_item表一次 JOIN 就能拿到所有更换记录,不用N+1查询日志表;
  • 日志字段可为空,不影响主流程;
  • 未来扩展字段(如拍照存证 URL)只需改 JSON 结构,不改表结构。

4. 避坑:维修系统上线前必须踩过的 4 个深坑,每一条都来自真实翻车现场

4.1 现象:扫码枪扫出的 SN 码末尾多了个回车符(\r\n),导致SELECT * FROM device WHERE sn = ?查不到设备

原因:Windows 系统下串口扫码枪默认以\r\n结束,而 Java 的Scanner.nextLine()或BufferedReader.readLine()会吞掉\n但保留\r,最终sn字段存了"F123456789\r"。
解决:在 Controller 接收参数后立即清洗:

@PostMapping("/scan") public Result scanDevice(@RequestParam String sn) { String cleanSn = sn.trim().replaceAll("[\\r\\n\\t]", ""); // 清除所有控制字符 Device device = deviceService.getBySn(cleanSn); if (device == null) { return Result.fail("未找到设备:" + cleanSn); } return Result.success(device); }

4.2 现象:师傅用安卓 Pad 提交工单,部分中文显示为问号(????)

原因:MySQL 表和字段的字符集是latin1,而非utf8mb4;或 JDBC URL 缺少characterEncoding=utf8mb4参数。
解决:

  1. 登录 MySQL 执行:
ALTER DATABASE repair_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE device CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  1. 检查application.yml中datasource.url是否包含characterEncoding=utf8mb4(必须显式声明):
url: jdbc:mysql://localhost:3306/repair_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4

4.3 现象:导出 Excel 报表时,日期列显示为数字(如 44820),而非“2022-09-15”

原因:使用 Apache POI 导出时,直接cell.setCellValue(LocalDateTime.now()),POI 将其识别为 Excel 的日期序列号(1900-01-01 起的天数),但单元格格式未设为日期。
解决:创建日期样式并应用:

CellStyle dateStyle = workbook.createCellStyle(); CreationHelper createHelper = workbook.getCreationHelper(); dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-mm-dd")); cell.setCellStyle(dateStyle); cell.setCellValue(Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()));

4.4 现象:系统运行一周后,MySQL 连接池报错 “Too many connections”,服务假死

原因:MyBatis-Plus 的LambdaQueryWrapper在链式调用中未关闭QueryWrapper,或 Service 方法未加@Transactional导致连接未归还。
解决:

  • 强制所有数据库操作走@Service方法,禁止在 Controller 中直接调用 Mapper;
  • 在application.yml中设置连接池硬约束:
spring: datasource: hikari: maximum-pool-size: 20 # 严格限制最大连接数 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

5. 让维修师傅愿意用:用极简 UI + 离线缓存 + 微信通知,绕过“他们不想学新系统”的心理防线

5.1 前端不做 SPA,用 Thymeleaf + Bootstrap 5 构建“三页流”界面

维修师傅不是程序员,他们需要的是:打开浏览器 → 扫码 → 点一下“开始维修” → 完事。我们放弃 Vue/React,用 Spring Boot 内置的 Thymeleaf 模板引擎,只做三页:

  • /index.html:大按钮“扫码接单”、“查看今日工单”、“配件库存”
  • /scan:全屏扫码区域,扫码后自动跳转/order/{id}
  • /order/{id}:工单详情页,顶部显示设备信息,中部大按钮“更换屏幕”、“更换电池”、“清洁保养”,底部“提交完成”

HTML 片段(/templates/order.html):

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${order.device.brand} + ${order.device.model} 维修"></title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container mt-4"> <h2>维修工单 #<span th:text="${order.id}"></span></h2> <div class="card mb-3"> <div class="card-body"> <h5 class="card-title" th:text="${order.device.brand} + ' ' + ${order.device.model}"></h5> <p class="card-text">SN:<span th:text="${order.device.sn}"></span></p> <p class="card-text">客户:<span th:text="${order.customer.name}"></span> | <span th:text="${order.customer.phone}"></span></p> </div> </div> <!-- 维修动作按钮组 --> <div class="btn-group w-100 mb-3" role="group"> <button type="button" class="btn btn-primary btn-lg flex-fill" onclick="doAction('screen')"> 更换屏幕 </button> <button type="button" class="btn btn-success btn-lg flex-fill" onclick="doAction('battery')"> 更换电池 </button> <button type="button" class="btn btn-warning btn-lg flex-fill" onclick="doAction('clean')"> 清洁保养 </button> </div> <div class="alert alert-info"> <strong>提示:</strong>点击按钮即执行配件扣减、状态更新,并发送微信通知。 </div> </div> <script th:inline="javascript"> function doAction(action) { fetch('/api/order/' + [[${order.id}]] + '/action', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({action: action}) }).then(r => r.json()).then(data => { if (data.success) { alert('操作成功!已通知客户'); location.reload(); // 刷新页面看最新状态 } else { alert('操作失败:' + data.msg); } }); } </script> </body> </html>

注意:所有按钮点击后,前端不跳转,只fetch提交,成功后location.reload()—— 这是最符合师傅直觉的操作:点一下,等个弹窗,刷新就行。

5.2 微信模板消息:用企业微信免开发接入,5 分钟开通通知能力

维修店不需要自建公众号或小程序。我们直接对接企业微信(免费、无需认证、支持个人手机号登录):

  1. 在 企业微信管理后台 创建「维修通知」应用,获取AgentId和Secret;
  2. 在application.yml中配置:
wechat: corp-id: ww1234567890abcdef agent-id: 1000001 secret: aBcDeFgHiJkLmNoPqRsTuVwXyZ
  1. 发送消息的 Service(调用企业微信 API):
@Service public class WechatNotifyService { @Value("${wechat.corp-id}") private String corpId; @Value("${wechat.agent-id}") private Integer agentId; @Value("${wechat.secret}") private String secret; public void sendRepairComplete(String openId, String device, String orderNo) { // 1. 获取 access_token String tokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + secret; String tokenJson = restTemplate.getForObject(tokenUrl, String.class); String accessToken = JsonPath.read(tokenJson, "$.access_token"); // 2. 发送模板消息 String msgUrl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken; Map<String, Object> payload = new HashMap<>(); payload.put("touser", openId); payload.put("msgtype", "template_card"); payload.put("agentid", agentId); payload.put("template_card", Map.of( "card_type", "text_notice", "source", Map.of("icon_url", "https://example.com/icon.png", "desc", "XX数码维修"), "main_title", Map.of("title", "维修完成通知"), "emphasis_content", Map.of("title", "请取机", "desc", "您的" + device + "已维修完毕"), "quote_area", Map.of("type", 1, "url", "https://example.com/order/" + orderNo, "appid", "", "pagepath", ""), "sub_title_text", "工单号:" + orderNo + ",取机时间:即刻起", "horizontal_content_list", Arrays.asList( Map.of("keyname", "维修项目", "value", "屏幕更换+贴膜"), Map.of("keyname", "费用", "value", "¥280.00"), Map.of("keyname", "取机地址", "value", "XX数码城二楼208") ), "jump_list", Arrays.asList( Map.of("type", 1, "url", "https://example.com/order/" + orderNo, "title", "查看工单详情") ) )); restTemplate.postForObject(msgUrl, payload, String.class); } }

提示:openId不是微信个人号 openid,而是企业微信中客户联系功能分配的external_userid。维修员添加客户微信时,用企业微信「客户联系」功能,系统自动同步客户身份,openId即可从回调事件中获取。这一步无需前端开发,店长在后台点几下就配好。

5.3 离线缓存策略:让师傅在断网时仍能查历史工单

维修店网络不稳定是常态。我们用 HTML5Cache Manifest(虽已废弃,但在内网环境稳定可靠)实现离线访问:

  1. 创建cache.manifest文件(放在src/main/resources/static/下):
CACHE MANIFEST # v20240615 CACHE: /index.html /css/bootstrap.min.css /js/bootstrap.bundle.min.js /favicon.ico NETWORK: /api/
  1. 在index.html<html>标签中添加:
<html manifest="/cache.manifest">
  1. 启动时,浏览器会下载CACHE列表中的资源。断网后,访问/index.html仍能显示首页和按钮,点击“查看今日工单”会提示“网络不可用,请稍后重试”——比白屏友好十倍。

6. 最后一个技巧:用 Logback 的 SiftingAppender,按维修门店分日志文件,老板查问题不再需要翻三天日志

维修连锁往往有多个门店(A店、B店、C店),共用一套后端系统。老板问:“昨天A店的屏幕更换单为什么没生成?”运维就得在application.log里 grep 三天——直到发现日志里混着 B 店的支付失败、C 店的库存告警。解决方案:用 Logback 的SiftingAppender,根据请求头中的X-Store-Id动态生成日志文件。

第一步,在src/main/resources/logback-spring.xml中配置:

<appender name="STORE_LOG" class="ch.qos.logback.core.sift.SiftingAppender"> <discriminator> <key>storeId</key> <defaultValue>unknown</defaultValue> </discriminator> <sift> <appender name="STORE-${storeId}" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/store_${storeId}.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/store_${storeId}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> </sift> </appender> <root level="INFO"> <appender-ref ref="STORE_LOG"/> </root>

第二步,写一个 Filter,从请求头提取X-Store-Id并放入 MDC(Mapped Diagnostic Context):

@Component public class StoreIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String storeId = httpRequest.getHeader("X-Store-Id"); if (storeId == null || storeId.trim().isEmpty()) { storeId = "unknown"; } MDC.put("storeId", storeId); // 这个 key 必须和 discriminator.key 一致 try { chain.doFilter(request, response); } finally { MDC.clear(); // 清理,避免线程复用污染 } } }

第三步,前端(或 Nginx)在每次请求头中带上门店标识:

curl -H "X-Store-Id: A_SHOP" http://localhost:8081/api/order/123

效果:日志自动分流到logs/store_A_SHOP.log、logs/store_B_SHOP.log……老板要查 A 店问题,直接tail -f logs/store_A_SHOP.log,5 秒定位。

这个技巧背后是运维思维:不要让业务方适应技术,而要用技术适配业务场景。维修系统不是炫技的玩具,它是师傅手边的螺丝刀、是老板抽屉里的记账本、是客户手机里的一条微信通知。我写这篇笔记时,正看着自己修了三年的 MacBook 上贴着的那张泛黄维修单——上面手写着“更换 SSD,¥320,张师傅,2021.08.12”。希望你搭起的系统,也能在某个县城数码城的旧电脑上,安静地跑下去,不崩溃、不丢数据、不难用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 19:55:13

Python+OpenCV本地运行识别图片中文件

环境搭建 pip install opencv-python numpy目录结构 chkimg.py models/ ├── efficientdet_d2/ # EfficientDet-D2 模型文件 │ ├── saved_model.pb # 模型文件 │ ├── variables/ # 模型变量 │ └── assets/ # 模型…

作者头像 李华
网站建设 2026/10/29 19:50:24

从U盘到根补丁:OpenCore Legacy Patcher 3阶段让老Mac装上最新macOS

从U盘到根补丁&#xff1a;OpenCore Legacy Patcher 3阶段让老Mac装上最新macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 那台被系统更新抛下的老 Mac&…

作者头像 李华
网站建设 2026/10/29 19:47:09

【FDE系列】阶段2:Day 43:Docker Compose — 多容器一键编排

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/29 19:44:16

MIMO检测之VBLAST接收机:ZF-SIC、MMSE-SQR原理与MATLAB实现

简介&#xff1a;一套面向MIMO-VBLAST系统解调算法验证的MATLAB仿真资源&#xff0c;基于排序QR分解改进MMSE检测&#xff0c;适合通信专业高年级学生、科研新手以及算法工程师快速开展对比实验与算法复现。代码由多个模块化脚本组成&#xff0c;完整覆盖发射信号生成、星座映射…

作者头像 李华