1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package cn.iocoder.yudao.module.mes.service.safety;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyMaterialMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyMaterialStockMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyMaterialCategoryEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
 
/**
 * 安全生产 - 应急物资台账与出入库 Service
 *
 * <p>覆盖电力绝缘用具、危化品、消防物资。出库时行锁校验库存,避免并发扣减为负。</p>
 *
 * @author 超级管理员
 */
@Service
@Validated
public class SafetyMaterialService {
 
    @Resource
    private SafetyMaterialMapper materialMapper;
    @Resource
    private SafetyRefResolver refResolver;
    @Resource
    private SafetyMaterialStockMapper stockMapper;
 
    // ==================== 物资台账 ====================
 
    public Long createMaterial(SafetyMaterialSaveReqVO reqVO) {
        reqVO.setId(null);
        String code = StrUtil.trim(reqVO.getCode());
        if (materialMapper.selectByCode(code) != null) {
            throw exception(SAFETY_MATERIAL_CODE_DUPLICATE);
        }
        SafetyMaterialDO material = BeanUtils.toBean(reqVO, SafetyMaterialDO.class);
        material.setCode(code);
        material.setName(StrUtil.trim(reqVO.getName()));
        material.setUnit(StrUtil.blankToDefault(reqVO.getUnit(), "个"));
        applyManager(material, reqVO.getManagerId());
        material.setStockQty(reqVO.getStockQty() == null ? BigDecimal.ZERO : reqVO.getStockQty());
        material.setSafeStock(reqVO.getSafeStock() == null ? BigDecimal.ZERO : reqVO.getSafeStock());
        materialMapper.insert(material);
        return material.getId();
    }
 
    public void updateMaterial(SafetyMaterialSaveReqVO reqVO) {
        SafetyMaterialDO old = validateMaterialExists(reqVO.getId());
        String code = StrUtil.trim(reqVO.getCode());
        SafetyMaterialDO byCode = materialMapper.selectByCode(code);
        if (byCode != null && !byCode.getId().equals(old.getId())) {
            throw exception(SAFETY_MATERIAL_CODE_DUPLICATE);
        }
        SafetyMaterialDO material = BeanUtils.toBean(reqVO, SafetyMaterialDO.class);
        material.setCode(code);
        material.setName(StrUtil.trim(reqVO.getName()));
        applyManager(material, reqVO.getManagerId());
        // 库存数量只能通过出入库单变动,禁止直接编辑
        material.setStockQty(null);
        materialMapper.updateById(material);
    }
 
    public void deleteMaterial(Long id) {
        validateMaterialExists(id);
        materialMapper.deleteById(id);
    }
 
    public SafetyMaterialDO getMaterial(Long id) {
        return validateMaterialExists(id);
    }
 
    public PageResult<SafetyMaterialDO> getMaterialPage(SafetyMaterialPageReqVO reqVO) {
        return materialMapper.selectPage(reqVO);
    }
 
    /**
     * 保管人:从员工档案带出姓名落快照
     */
    private void applyManager(SafetyMaterialDO material, Long managerId) {
        material.setManagerId(managerId);
        material.setManager(managerId == null ? ""
                : refResolver.requireEmployee(managerId, SAFETY_EMPLOYEE_REQUIRED).getName());
    }
 
    /**
     * 批量回填保管人最新姓名
     */
    public void fillManagers(List<SafetyMaterialDO> materials) {
        if (CollUtil.isEmpty(materials)) {
            return;
        }
        var employeeMap = refResolver.employeeMap(materials.stream().map(SafetyMaterialDO::getManagerId).toList());
        materials.forEach(material -> {
            var employee = employeeMap.get(material.getManagerId());
            if (employee != null) material.setManager(employee.getName());
        });
    }
 
    public static void fillMaterialDisplay(SafetyMaterialDO material, SafetyMaterialRespVO respVO) {
        respVO.setCategoryName(SafetyMaterialCategoryEnum.getNameByCategory(material.getCategory()));
        BigDecimal stock = material.getStockQty() == null ? BigDecimal.ZERO : material.getStockQty();
        BigDecimal safe = material.getSafeStock() == null ? BigDecimal.ZERO : material.getSafeStock();
        respVO.setBelowSafeStock(safe.signum() > 0 && stock.compareTo(safe) < 0);
        if (material.getExpireDate() != null) {
            respVO.setExpireRemainingDays(ChronoUnit.DAYS.between(LocalDate.now(), material.getExpireDate()));
        }
    }
 
    // ==================== 出入库 ====================
 
    /**
     * 出入库登记:行锁读取库存 → 计算变动 → 写流水 → 回写库存
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createStock(SafetyMaterialStockSaveReqVO reqVO) {
        SafetyMaterialDO material = materialMapper.selectByIdForUpdate(reqVO.getMaterialId());
        if (material == null) {
            throw exception(SAFETY_MATERIAL_NOT_EXISTS);
        }
        BigDecimal before = material.getStockQty() == null ? BigDecimal.ZERO : material.getStockQty();
        boolean inbound = "IN".equals(reqVO.getBizType());
        BigDecimal after = inbound ? before.add(reqVO.getQuantity()) : before.subtract(reqVO.getQuantity());
        if (!inbound && after.signum() < 0) {
            throw exception(SAFETY_MATERIAL_STOCK_NOT_ENOUGH);
        }
        SafetyMaterialStockDO stock = BeanUtils.toBean(reqVO, SafetyMaterialStockDO.class);
        stock.setId(null);
        stock.setMaterialName(material.getName());
        stock.setCategory(material.getCategory());
        stock.setQuantity(reqVO.getQuantity());
        stock.setStockBefore(before);
        stock.setStockAfter(after);
        stock.setBizDate(reqVO.getBizDate() == null ? LocalDate.now() : reqVO.getBizDate());
        stock.setOperator(StrUtil.trim(reqVO.getOperator()));
        stockMapper.insert(stock);
 
        SafetyMaterialDO update = new SafetyMaterialDO();
        update.setId(material.getId());
        update.setStockQty(after);
        materialMapper.updateById(update);
        return stock.getId();
    }
 
    public PageResult<SafetyMaterialStockDO> getStockPage(SafetyMaterialStockPageReqVO reqVO) {
        return stockMapper.selectPage(reqVO);
    }
 
    public static void fillStockDisplay(SafetyMaterialStockDO stock, SafetyMaterialStockRespVO respVO) {
        respVO.setCategoryName(SafetyMaterialCategoryEnum.getNameByCategory(stock.getCategory()));
        respVO.setBizTypeName("IN".equals(stock.getBizType()) ? "入库" : "出库");
    }
 
    /**
     * 预警清单:低于安全库存的物资
     */
    public List<SafetyMaterialDO> getBelowSafeStockList() {
        return materialMapper.selectBelowSafeStock();
    }
 
    /**
     * 预警清单:有效期临近(默认 30 天内)的物资
     */
    public List<SafetyMaterialDO> getExpiringList(Integer days) {
        int warnDays = days == null || days <= 0 ? 30 : days;
        return materialMapper.selectExpiringBefore(LocalDate.now().plusDays(warnDays));
    }
 
    private SafetyMaterialDO validateMaterialExists(Long id) {
        SafetyMaterialDO material = id == null ? null : materialMapper.selectById(id);
        if (material == null) {
            throw exception(SAFETY_MATERIAL_NOT_EXISTS);
        }
        return material;
    }
 
}