news 2026/7/6 7:39:40

实习总结复习

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实习总结复习

1.报工审批模块

1.我当时负责了报工后的审批模块。如果工人报工后,需要主管进行审批,审批通过后要同时触发对应的物料消耗扣减、产品和产品和成品入库的数据更新以及工单和任务的进度更新

这点当时的难度对我来说还是挺有挑战的,因为审批这个单次操作的操作就涉及了物料、仓库、和生产进度这3个领域,且还涉及到了6张表的更新

当时的主要的业务逻辑是根据对应的报工单去查询对应的工艺路线,得到两个keyFlag和checkFlag,接着就要去仓库中扣料,然后接着要根据keyFlag的值判断是否是关键工序:

如果不是关键工序的话,因为非关键工序没有设计到物料的消耗,也没有产生和产出,此时直接不入库了,直接结束即可。

如果是关键工序的话,还要考虑判断是否需要质检,如果不需要质检,直接根据工单中填的合格品数量和不合格品数量,分别生成产出行:合格品入成品库,不合格品入废品库,两条行都生成对应的库存的入库流水

如果需要质检的话,这是最复杂的,此时系统先完成了物料消耗,生成一条“待检”状态的产出行,此时报工单的状态就变为了“带检验”,此时直接返回,这时候物料已经扣了,但是产品没有入库,需要等待QC任务质检完成之后,才能入库

等QC人员完成检验后,QC就会表面合格和不合格的数量,此时就可以将原来的待检行拆成两行:合格品行入库到成品区,不合格品走废品流程,同时会写报工单的合格/不合格数量,接着去更新工单和任务进度

因为这个业务的涉及的状态转换和表有点多,此时的状态转换我是通过一个状态机来严格控制状态的扭转顺序,保证状态的正确扭转,且你要保证如果这其中的某一步出错的话,对应的数据库表的数据也要回滚,这里我是通过一个整体事务,用@Transactional注解将所有涉及到的操作都包成一个整体,这样如果里面的任何一部出现异常,此时自动回滚对应的数据库表数据

第一层是状态扭转控制。我没有用第三方的状态机框架,就是用枚举定义好工单的几种状态,然后在每个操作方法里先查一次数据库拿到当前状态,if 判断是否允许转换。比如工单完成操作只允许 CONFIRMED 状态才能执行,如果不是直接抛异常。这样虽然代码看起来是 if-else,但逻辑上是严格的状态校验,而且不用引入额外的框架依赖。

第一步:定义状态和事件枚举

public enum FeedbackState { PREPARE, // 草稿 APPROVING, // 审批中 UNCHECK, // 待检验 FINISHED // 已完成 } ​ public enum FeedbackEvent { SUBMIT, // 提交审批 REJECT, // 驳回 APPROVE, // 审批通过(非质检工序→直接完成) APPROVE_QC, // 审批通过(质检工序→待检验) QC_PASS // 质检完成回写 }

第二步:配置状态转换规则,Action 挂载到转换上

@Configuration @EnableStateMachine(name = "feedbackStateMachine") public class FeedbackStateMachineConfig extends StateMachineConfigurerAdapter<FeedbackState, FeedbackEvent> { ​ @Resource private SubmitAction submitAction; @Resource private RejectAction rejectAction; @Resource private ApproveFinishAction approveFinishAction; // 非质检工序:直接完成 @Resource private ApproveToQcAction approveToQcAction; // 质检工序:进入待检验 @Resource private QcPassAction qcPassAction; // 质检完成:拆行+入库+进度 ​ @Override public void configure(StateMachineStateConfigurer<FeedbackState, FeedbackEvent> states) { states.withStates() .initial(FeedbackState.PREPARE) .states(EnumSet.allOf(FeedbackState.class)); } ​ @Override public void configure(StateMachineTransitionConfigurer<FeedbackState, FeedbackEvent> transitions) { transitions // 1. 提交审批:草稿 → 审批中 .withExternal() .source(FeedbackState.PREPARE) .target(FeedbackState.APPROVING) .event(FeedbackEvent.SUBMIT) .action(submitAction) .and() // 2. 驳回:审批中 → 草稿 .withExternal() .source(FeedbackState.APPROVING) .target(FeedbackState.PREPARE) .event(FeedbackEvent.REJECT) .action(rejectAction) .and() // 3. 审批通过(非关键工序/非质检工序):审批中 → 已完成 .withExternal() .source(FeedbackState.APPROVING) .target(FeedbackState.FINISHED) .event(FeedbackEvent.APPROVE) .action(approveFinishAction) .and() // 4. 审批通过(关键+质检工序):审批中 → 待检验 .withExternal() .source(FeedbackState.APPROVING) .target(FeedbackState.UNCHECK) .event(FeedbackEvent.APPROVE_QC) .action(approveToQcAction) .and() // 5. 质检完成回写:待检验 → 已完成 .withExternal() .source(FeedbackState.UNCHECK) .target(FeedbackState.FINISHED) .event(FeedbackEvent.QC_PASS) .action(qcPassAction); } }

第三步:Action 里写真正的业务逻辑

现在每个 Action 只关心"状态变完之后要做什么",不关心"当前状态允不允许"——那是框架干的。

//SubmitAction: 提交审批(只改状态,无级联操作) @Component public class SubmitAction implements StateMachineAction<FeedbackState, FeedbackEvent> { ​ @Resource private MesProFeedbackMapper feedbackMapper; ​ @Override public void execute(StateContext<FeedbackState, FeedbackEvent> context) { Long feedbackId = context.getMessage().getHeaders().get("feedbackId", Long.class); feedbackMapper.updateById(new MesProFeedbackDO() .setId(feedbackId) .setStatus(FeedbackState.APPROVING.name())); } }
//ApproveFinishAction: 审批通过(非质检工序)→ 物料消耗 + 入库 + 进度 @Component public class ApproveFinishAction implements StateMachineAction<FeedbackState, FeedbackEvent> { ​ @Resource private MesWmItemConsumeService itemConsumeService; @Resource private MesWmProductProduceService productProduceService; @Resource private MesProFeedbackMapper feedbackMapper; @Resource private MesProTaskService taskService; @Resource private MesProWorkOrderService workOrderService; ​ @Override public void execute(StateContext<FeedbackState, FeedbackEvent> context) { Long feedbackId = context.getMessage().getHeaders().get("feedbackId", Long.class); ​ // 1. 物料消耗 MesWmItemConsumeDO consume = itemConsumeService.generateItemConsume(feedbackId); if (consume != null) { itemConsumeService.finishItemConsume(consume.getId()); } ​ // 2. 产成品入库 MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedbackId, false); productProduceService.finishProductProduce(produce.getId()); ​ // 3. 更新任务/工单进度 MesProFeedbackDO feedback = feedbackMapper.selectById(feedbackId); taskService.updateProducedQuantity(feedback.getTaskId(), ...); workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), ...); ​ // 4. 更新状态为 FINISHED feedbackMapper.updateById(new MesProFeedbackDO() .setId(feedbackId).setStatus(FeedbackState.FINISHED.name())); } }
//ApproveToQcAction: 审批通过(质检工序)→ 只生成待检产出行,不入库 @Component public class ApproveToQcAction implements StateMachineAction<FeedbackState, FeedbackEvent> { ​ @Resource private MesWmProductProduceService productProduceService; @Resource private MesProFeedbackMapper feedbackMapper; ​ @Override public void execute(StateContext<FeedbackState, FeedbackEvent> context) { Long feedbackId = context.getMessage().getHeaders().get("feedbackId", Long.class); ​ // 1. 生成待检产出行(qualityStatus = PENDING) productProduceService.generateProductProduce(feedbackId, true); ​ // 2. 状态改为 UNCHECK feedbackMapper.updateById(new MesProFeedbackDO() .setId(feedbackId).setStatus(FeedbackState.UNCHECK.name())); } }
//QcPassAction: 质检完成 → 拆合格/不合格行 + 入库 + 进度 @Component public class QcPassAction implements StateMachineAction<FeedbackState, FeedbackEvent> { ​ @Resource private MesWmProductProduceService productProduceService; @Resource private MesProFeedbackMapper feedbackMapper; @Resource private MesProTaskService taskService; ​ @Override public void execute(StateContext<FeedbackState, FeedbackEvent> context) { Long feedbackId = context.getMessage().getHeaders().get("feedbackId", Long.class); Long sourceLineId = context.getMessage().getHeaders().get("sourceLineId", Long.class); BigDecimal qualifiedQty = context.getMessage().getHeaders().get("qualifiedQty", BigDecimal.class); BigDecimal unqualifiedQty = context.getMessage().getHeaders().get("unqualifiedQty", BigDecimal.class); ​ // 1. 拆分待检产出行 productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty); ​ // 2. 更新报工单状态为 FINISHED + 回填合格/不合格数量 feedbackMapper.updateById(new MesProFeedbackDO() .setId(feedbackId) .setQualifiedQuantity(qualifiedQty) .setUnqualifiedQuantity(unqualifiedQty) .setStatus(FeedbackState.FINISHED.name())); ​ // 3. 更新任务/工单进度 MesProFeedbackDO feedback = feedbackMapper.selectById(feedbackId); taskService.updateProducedQuantity(feedback.getTaskId(), ...); workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), ...); } }

第四步持久化——状态机重启后从数据库恢复

Spring State Machine 默认所有状态存在内存里,服务器重启就丢了。报工单的状态必须持久化到数据库,所以需要一个 Persist 层:

@Component public class FeedbackStatePersist implements StateMachinePersist<FeedbackState, FeedbackEvent, Long> { ​ @Resource private MesProFeedbackMapper feedbackMapper; ​ @Override public void write(StateMachineContext<FeedbackState, FeedbackEvent> context, Long feedbackId) { // 状态变更时:把当前状态写入数据库 feedbackMapper.updateById(new MesProFeedbackDO() .setId(feedbackId) .setStatus(context.getState().name())); } ​ @Override public StateMachineContext<FeedbackState, FeedbackEvent> read(Long feedbackId) { // 启动时:从数据库恢复状态 MesProFeedbackDO feedback = feedbackMapper.selectById(feedbackId); if (feedback == null) return null; return new DefaultStateMachineContext<>( FeedbackState.valueOf(feedback.getStatus()), null, null, null, null); } }

然后通过拦截器让每次状态变更自动持久化:

public class PersistingStateChangeInterceptor extends StateMachineInterceptorAdapter<FeedbackState, FeedbackEvent> { ​ @Resource private FeedbackStatePersist persist; ​ @Override public void preStateChange(StateContext<FeedbackState, FeedbackEvent> context) { Long feedbackId = context.getMessage().getHeaders().get("feedbackId", Long.class); persist.write(context, feedbackId); } ​ @Override public void postStateChange(StateContext<FeedbackState, FeedbackEvent> context) { // post:状态已持久化完毕 } }

持久层不是用来"写"的,是用于服务重启后从数据库恢复状态机的内存状态,让状态机知道"你当前在哪个状态,别从头开始"。写库的事 Action 已经做了。

服务器重启后发生的事:

服务器启动 ↓ 一个请求过来:要审批通过 feedbackId = 42 的报工单 ↓ Service 层调用 stateMachineFactory.getStateMachine("42") ↓ Spring State Machine 创建了一个全新的内存状态机实例 ↓ 这个状态机不知道 feedback 42 当前是 PREPARE 还是 APPROVING 还是 FINISHED ↓ 默认从 initial 状态(PREPARE)开始 ↓ 如果此时发一个 APPROVE 事件 → 状态机从 PREPARE 成功走到 APPROVING ​ ---但数据库里这个报工单实际已经是 FINISHED 了--- ---状态机认为它在 APPROVING,数据库认为它是 FINISHED--- ---后面再发一个 QC_PASS 事件,状态机还能继续走,实际上已经乱套了---

第五步 改造后的 Service,不再手动写状态校验,全部交给状态机框架

@Service public class MesProFeedbackServiceImpl { ​ @Resource private StateMachineFactory<FeedbackState, FeedbackEvent> stateMachineFactory; @Resource private FeedbackStatePersist statePersist; ​ @Override @Transactional(rollbackFor = Exception.class) public boolean approveFeedback(Long id) { // 1. 查询报工单的业务属性(决定用哪个 Event) MesProFeedbackDO feedback = feedbackMapper.selectById(id); MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( feedback.getRouteId(), feedback.getProcessId()); boolean keyFlag = Boolean.TRUE.equals(routeProcess.getKeyFlag()); boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag()); ​ // 2. 根据业务属性选择事件 FeedbackEvent event; if (keyFlag && checkFlag) { event = FeedbackEvent.APPROVE_QC; // 关键+质检工序 → 待检验 } else { event = FeedbackEvent.APPROVE; // 其他 → 直接完成 } ​ // 3. 构建消息 Message<FeedbackEvent> message = MessageBuilder .withPayload(event) .setHeader("feedbackId", id) .build(); ​ // 4. 获取状态机,恢复状态,发送事件 StateMachine<FeedbackState, FeedbackEvent> sm = stateMachineFactory.getStateMachine(id.toString()); sm.stop(); sm.getStateMachineAccessor() .withRegion() .addStateMachineInterceptor(new PersistingStateChangeInterceptor()); statePersist.read(sm, id); sm.start(); ​ boolean success = sm.sendEvent(message); if (!success) { throw exception(PRO_FEEDBACK_STATUS_ERROR); } return FeedbackState.FINISHED.equals(sm.getState().getId()); } }

使用状态机的好处:

  • 好处一:状态流转规则集中在一个地方,不会散落,如果以后有些单据的状态流转的规则发生了改变,此时就很容器去修改了,只需要去调整状态转换配置即可

  • 好处二:非法流转被框架拦截,而不是靠人记得写 if。

第二层是事务一致性。我把所有级联操作放到同一个方法里,在方法上加了@Transactional(rollbackFor = Exception.class)。比如 finishWorkOrder 方法里,先是遍历完成该工单下的所有子任务,再更新工单状态为已完成。任何一个子任务抛异常(比如任务状态不对),整个事务回滚,工单状态不会变成已完成,保证数据一致。

级联操作就是一个入口方法,内部按顺序自动调用多个关联表的操作方法

2.策略模式实现编码

2.第二个的话,就是当时mes系统中有很多单据,比如工单、质检单、入库单、批次号等,不同的单据编码规则还不一样,有些事固定前缀+日期+流水号,有的还要带上物料分类,我一开始做的时候就是给直接给不同的单据模块写一个生成单号的方法,逻辑都是拼接字符串+数据库去max流水号,我写着写着就发现有点奇怪,这些编码的逻辑差不多是一样了,写了很多重复的代码,但是代码洁癖发作了,而且当时还考虑到如果以后有增加了很多单据,那么岂不是又要写很多重复的代码,当时就去优化一下,后面的话就想到了那个策略模式,我把编码拆分成四种基本片段:固定字符串、日期、用户输入的字符、依旧流水号。每种片段都独立设计成一个策略类,都实现同一个策略接口,此时生成编码的时候,此时只要去数据库读取配置号的规则和字符,按顺序遍历,每段调用对应的策略生成对应的内容,然后再拼接起来即可

我这样改完之后,就省去了很多重复的代码,我看起来也舒服多了,而且用例策略模式,如果后续新增一种单据编码,此时只需要在数据库中配置表中添加对应的编码信息接口,后端哪里此时也只用在去增加一个新的策略类去实现策略接口,此时就不用对整体的业务逻辑进行很大的调整,这样代码的可扩展性也很高了

第一步:抽象策略接口

// 4 种片段类型 enum MesMdAutoCodePartTypeEnum { FIXED_CHAR(3, "固定字符"), // 比如 "WO-" DATE(2, "当前日期"), // 比如 "20260705" INPUT_CHAR(1, "输入字符"), // 比如物料编码 "MAT001" SERIAL_NUMBER(4, "流水号") // 比如 "0001" } ​ // 策略接口 public interface MesMdAutoCodePartStrategy { Integer getType(); // 返回类型,用于匹配 String generate(MesMdAutoCodePartDO part, MesMdAutoCodeContext context); }

第二步:四种策略各自实现

固定字符策略:最简单,返回配好的值

@Component public class MesMdAutoCodeFixedCharPartStrategy implements MesMdAutoCodePartStrategy { ​ @Override public Integer getType() { return MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType(); // 3 } ​ @Override public String generate(MesMdAutoCodePartDO part, MesMdAutoCodeContext context) { // 直接从配置取固定字符,比如 "WO-" return StrUtil.emptyToDefault(part.getFixCharacter(), ""); } }

日期策略:按配置的格式返回当前时间

@Component public class MesMdAutoCodeDatePartStrategy implements MesMdAutoCodePartStrategy { ​ @Override public Integer getType() { return MesMdAutoCodePartTypeEnum.DATE.getType(); // 2 } ​ @Override public String generate(MesMdAutoCodePartDO part, MesMdAutoCodeContext context) { String dateFormat = StrUtil.emptyToDefault(part.getDateFormat(), "yyyyMMdd"); return DateUtil.format(LocalDateTime.now(), dateFormat); } }

输入字符策略:返回调用方传入的参数

@Component public class MesMdAutoCodeInputCharPartStrategy implements MesMdAutoCodePartStrategy { ​ @Override public Integer getType() { return MesMdAutoCodePartTypeEnum.INPUT_CHAR.getType(); // 1 } ​ @Override public String generate(MesMdAutoCodePartDO part, MesMdAutoCodeContext context) { // 调用方传进来的参数,比如物料编码 return StrUtil.emptyToDefault(context.getInputChar(), ""); } }

流水号策略:最核心的,Redis INCR 自增 + 按周期重置

@Component public class MesMdAutoCodeSerialNumberPartStrategy implements MesMdAutoCodePartStrategy { ​ @Resource private MesMdAutoCodeRedisDAO redisDAO; ​ @Override public Integer getType() { return MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType(); // 4 } ​ @Override public String generate(MesMdAutoCodePartDO part, MesMdAutoCodeContext context) { // key 后缀:支持按天/月/年归零 String keySuffix = buildRedisKeySuffix(part, context); // 比如 key = "auto_code:3:20260705" Integer step = part.getSerialStep() != null ? part.getSerialStep() : 1; Integer startNo = part.getSerialStartNo() != null ? part.getSerialStartNo() : 1; Long serialNo = redisDAO.increment(keySuffix, duration, startNo, step); ​ // 补零:长度不够左边补 0 String serialStr = String.valueOf(serialNo); if (serialStr.length() < part.getLength()) { serialStr = StrUtil.padPre(serialStr, part.getLength(), '0'); } context.setSerialNo(serialNo); return serialStr; } }

第三步:@PostConstruct 自动注入所有策略

@Service public class MesMdAutoCodeRecordServiceImpl implements MesMdAutoCodeRecordService { ​ @Resource private List<MesMdAutoCodePartStrategy> strategyList; // Spring 自动注入所有策略实现 ​ private Map<Integer, MesMdAutoCodePartStrategy> strategyMap; ​ @PostConstruct public void init() { // type → 策略 的映射 // FIXED_CHAR(3) → FixedCharPartStrategy // DATE(2) → DatePartStrategy // INPUT_CHAR(1) → InputCharPartStrategy // SERIAL_NUMBER(4) → SerialNumberPartStrategy strategyMap = CollectionUtils.convertMap(strategyList, MesMdAutoCodePartStrategy::getType); } }

这里用到了@PostConstruct + List 注入——Spring 会自动收集所有MesMdAutoCodePartStrategy的实现类(4 个 Component),然后 init 时按 type 放到 Map 里。如果以后加第 5 种策略,只需要新加一个 Component 类,一行现有代码都不用改

第四步:编排引擎按配置顺序拼接

@Override @Transactional public String generateAutoCode(String ruleCode, String inputChar) { // 1. 从数据库查出编码规则和它的各个片段列表(按 sort 排序) MesMdAutoCodeRuleDO rule = ruleService.getAutoCodeRuleByCode(ruleCode); List<MesMdAutoCodePartDO> parts = partService.getAutoCodePartListByRuleId(rule.getId()); ​ // 2. 遍历每个片段,按 type 找到对应的策略执行 MesMdAutoCodeContext context = new MesMdAutoCodeContext() .setRule(rule).setParts(parts).setInputChar(inputChar); ​ StringBuilder codeBuilder = new StringBuilder(); for (MesMdAutoCodePartDO part : parts) { // 从 Map 中取出对应策略 MesMdAutoCodePartStrategy strategy = strategyMap.get(part.getType()); // 策略生成内容 String partCode = strategy.generate(part, context); // 截断到指定长度 partCode = StrUtil.sub(partCode, 0, part.getLength()); codeBuilder.append(partCode); } ​ // 3. 补全到最大长度(可选) String result = codeBuilder.toString(); if (Boolean.TRUE.equals(rule.getPadded())) { result = padCode(result, rule); } ​ // 4. 二次校验 + 保存记录 // ... return result; }

3Redis INCR指令生成流水号

3.还有那个用INCR原子性解决流水号生成重复的问题,我一开始写的当时生成流水号的逻辑是从数据库中查最大的流水号,然后加1,然后插入对应的流水号表。

后来上线的时候报了个错误,前端提示“编码重复,保存失败”,我第一反应是去查数据库,找到那两条重复的工单记录,发现它们的编码一模一样,都是WO-20260304-0015,但创建时间差了不到 50 毫秒,然后我去翻那个时间点的应用日志,看到第一条 insert 成功,第二条 insert 被 MySQL 的唯一索引挡住了,抛了个DuplicateEntryException。说明两个请求几乎是同时进来的,因为我写的流水号生成逻辑是“从数据库中查最大的流水号,然后加1”,这个 查询最大流水号的select和后面的 insert 之间没有加任何锁。两个线程同时查到同一个最大值,各自加 1 算出同样的流水号,此时就撞上了

当时为了证明这个观点,我还用JMeter模拟了20个线程并发的创建工单,然后就发现这个流水号重复生成的问题复现了,定位清楚之后,我当时不想加锁,我觉得这种流水号生成是一种非常频繁的操作,加锁的效率会很低,此时我就想到用Redis的INCR指令,这种就是天然的编码流水号重复生成的方案嘛,能够保证每次自增都是原子的,且后面我发现流水号的归零重置也很容易了,此时只需要在key的设计上按照规则ID加日期后缀,这样不同天生成的流水号都可以是从0开始的

4.Reddision分布式锁防止库存扣减

@Service public class MesWmTransactionServiceImpl implements MesWmTransactionService { @Resource private RedissonClient redissonClient; @Resource private MesWmMaterialStockMapper materialStockMapper; @Resource private MesWmTransactionMapper transactionMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createTransaction(MesWmTransactionSaveReqDTO reqDTO) { // 1. 锁外:前置校验(物料存在性、仓库冻结等) MesWmTransactionTypeEnum typeEnum = MesWmTransactionTypeEnum.valueOf(reqDTO.getType()); if (typeEnum == null) { throw exception(WM_TRANSACTION_TYPE_NOT_EXISTS); } // 2. 获取或创建库存记录(这一步可以在锁外做,因为 getOrCreate 不涉及数量变更) MesWmMaterialStockDO materialStock = materialStockService.getOrCreateMaterialStock( reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(), reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime()); // 3. 加分布式锁,key 按物料 + 批次粒度 String lockKey = "stock:" + reqDTO.getItemId() + ":" + reqDTO.getBatchId(); RLock lock = redissonClient.getLock(lockKey); // 尝试获取锁,等待 3 秒,持有锁 10 秒(看门狗自动续期) boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!acquired) { throw exception(WM_TRANSACTION_LOCK_ACQUIRE_FAILED); } try { // 4. 锁内:重新查询库存(拿到最新值) MesWmMaterialStockDO latestStock = materialStockMapper.selectById(materialStock.getId()); // 5. 扣减校验:如果是出库操作,判断库存是否够 if (!typeEnum.isInbound()) { // delta 是负数,直接用当前库存 + delta 判断 if (latestStock.getQuantity().add(reqDTO.getQuantity()).compareTo(BigDecimal.ZERO) < 0) { throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); } } // 6. 执行库存更新(这里不用 CAS 了,因为分布式锁已经串行化了) materialStockMapper.updateQuantityDirect(latestStock.getId(), reqDTO.getQuantity()); // 7. 插入事务流水 MesWmTransactionDO transaction = MesWmTransactionDO.builder() .type(reqDTO.getType()) .quantity(reqDTO.getQuantity()) .itemId(reqDTO.getItemId()) .batchId(reqDTO.getBatchId()) .materialStockId(materialStock.getId()) .bizType(reqDTO.getBizType()) .bizId(reqDTO.getBizId()) .bizCode(reqDTO.getBizCode()) .build(); transactionMapper.insert(transaction); } finally { // 8. 释放锁 lock.unlock(); } return transaction.getId(); } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 7:39:34

看到优秀网页不必从零重写:用Open-Lovable 快速生成可修改的前端原型

前言&#xff1a; 前端开发中&#xff0c;最耗时间的工作不一定是实现复杂功能&#xff0c;有时反而是把一个已经明确的页面设计重新搭建出来。 导航栏怎么排列、首屏内容如何布局、卡片之间保持多少间距、移动端又该怎样适配&#xff0c;这些细节单独看都不难&#xff0c;但从…

作者头像 李华
网站建设 2026/7/6 7:38:06

PIC18微控制器扩展EEPROM存储方案与I2C驱动实现

1. 为什么需要扩展存储空间&#xff1f;在嵌入式系统开发中&#xff0c;存储空间不足是个常见痛点。PIC18F96J94这类微控制器虽然功能强大&#xff0c;但内置的Flash和EEPROM容量有限&#xff08;通常只有几十到几百KB&#xff09;。当项目需要记录大量配置参数、历史数据或固件…

作者头像 李华
网站建设 2026/7/6 7:37:41

LV3296与PIC18F4680构建高性价比条码采集终端方案

1. LV3296与PIC18F4680的硬件协同架构解析这套组合的核心价值在于构建了一个高性价比的信息采集终端方案。LV3296作为专用条形码扫描模块&#xff0c;通过光电转换将条码信息转化为数字信号&#xff0c;而PIC18F4680作为主控MCU&#xff0c;负责协议解析、数据校验和传输调度。…

作者头像 李华
网站建设 2026/7/6 7:37:30

5、手动 scp / rsync 部署

手动 scp / rsync 部署,指的是:开发者或运维人员在本地把前端 build 产物生成出来,然后通过命令行把 dist 目录上传到服务器指定目录,由 Nginx、Apache 或其他静态服务对外提供访问。 对于 Vue 前端项目来说,典型流程就是: npm run build scp 或 rsync 上传 dist 服务器…

作者头像 李华