推送项目重构代码
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.oa.audit.mapper.OaWarehouseAuditLogMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 库房操作日志
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/oa/warehouseAudit")
|
||||
public class OaWarehouseAuditController extends BaseController {
|
||||
|
||||
private final OaWarehouseAuditLogMapper mapper;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<OaWarehouseAuditLog> list(OaWarehouseAuditLog query, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<OaWarehouseAuditLog> qw = Wrappers.lambdaQuery();
|
||||
if (query.getOpType() != null && !query.getOpType().isEmpty()) {
|
||||
qw.eq(OaWarehouseAuditLog::getOpType, query.getOpType());
|
||||
}
|
||||
if (query.getRefType() != null && !query.getRefType().isEmpty()) {
|
||||
qw.eq(OaWarehouseAuditLog::getRefType, query.getRefType());
|
||||
}
|
||||
if (query.getRefId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getRefId, query.getRefId());
|
||||
}
|
||||
if (query.getProjectId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getProjectId, query.getProjectId());
|
||||
}
|
||||
if (query.getWarehouseId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getWarehouseId, query.getWarehouseId());
|
||||
}
|
||||
if (query.getOpUserId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getOpUserId, query.getOpUserId());
|
||||
}
|
||||
qw.orderByDesc(OaWarehouseAuditLog::getOpTime);
|
||||
return TableDataInfo.build(mapper.selectPage(pageQuery.build(), qw));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 库房统一操作日志 sys_oa_warehouse_audit_log
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_oa_warehouse_audit_log")
|
||||
public class OaWarehouseAuditLog {
|
||||
|
||||
@TableId(value = "log_id")
|
||||
private Long logId;
|
||||
|
||||
/** 操作类型,见 {@link OpType} */
|
||||
private String opType;
|
||||
|
||||
/** 关联实体类型:requirement/task/master/warehouse */
|
||||
private String refType;
|
||||
|
||||
/** 关联实体 ID */
|
||||
private Long refId;
|
||||
|
||||
private Long projectId;
|
||||
private Long warehouseId;
|
||||
|
||||
private String summary;
|
||||
private String beforeJson;
|
||||
private String afterJson;
|
||||
|
||||
private Long opUserId;
|
||||
private String opUserName;
|
||||
private Date opTime;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.oa.audit.mapper.OaWarehouseAuditLogMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 业务侧统一打点入口。
|
||||
* 所有调用异步执行,失败不影响主业务。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OaWarehouseAuditService {
|
||||
|
||||
private final OaWarehouseAuditLogMapper mapper;
|
||||
|
||||
/** 最常用:单参摘要 */
|
||||
@Async
|
||||
public void log(String opType, String refType, Long refId, String summary) {
|
||||
log(opType, refType, refId, null, null, summary, null, null);
|
||||
}
|
||||
|
||||
/** 完整版 */
|
||||
@Async
|
||||
public void log(String opType, String refType, Long refId,
|
||||
Long projectId, Long warehouseId,
|
||||
String summary, String beforeJson, String afterJson) {
|
||||
try {
|
||||
OaWarehouseAuditLog entity = new OaWarehouseAuditLog();
|
||||
entity.setOpType(opType);
|
||||
entity.setRefType(refType);
|
||||
entity.setRefId(refId);
|
||||
entity.setProjectId(projectId);
|
||||
entity.setWarehouseId(warehouseId);
|
||||
entity.setSummary(summary == null ? "" : summary);
|
||||
entity.setBeforeJson(beforeJson);
|
||||
entity.setAfterJson(afterJson);
|
||||
try {
|
||||
entity.setOpUserId(LoginHelper.getUserId());
|
||||
entity.setOpUserName(LoginHelper.getNickName());
|
||||
} catch (Exception ignore) {
|
||||
// 定时任务/系统调用下无登录态
|
||||
entity.setOpUserName("system");
|
||||
}
|
||||
mapper.insert(entity);
|
||||
} catch (Exception e) {
|
||||
log.warn("[audit] write failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
30
ruoyi-oa/src/main/java/com/ruoyi/oa/audit/OpType.java
Normal file
30
ruoyi-oa/src/main/java/com/ruoyi/oa/audit/OpType.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
/**
|
||||
* 库房操作类型枚举。值用 String,便于库里直接读懂。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public final class OpType {
|
||||
private OpType() {}
|
||||
|
||||
public static final String REQ_CREATE = "REQ_CREATE"; // 采购需求新建
|
||||
public static final String REQ_UPDATE = "REQ_UPDATE"; // 采购需求修改
|
||||
public static final String REQ_DONE = "REQ_DONE"; // 采购需求完成
|
||||
public static final String REQ_CANCEL = "REQ_CANCEL"; // 采购需求取消
|
||||
public static final String REQ_DELETE = "REQ_DELETE"; // 采购需求删除
|
||||
|
||||
public static final String TASK_CREATE = "TASK_CREATE"; // 车间采购创建
|
||||
public static final String TASK_DONE = "TASK_DONE"; // 车间采购完成
|
||||
public static final String TASK_CANCEL = "TASK_CANCEL"; // 车间采购取消
|
||||
|
||||
public static final String IN = "IN"; // 入库
|
||||
public static final String OUT = "OUT"; // 出库
|
||||
public static final String RETURN = "RETURN"; // 退库
|
||||
public static final String STOCK_ADJUST = "STOCK_ADJUST"; // 库存盘点/修正
|
||||
|
||||
public static final String REF_REQUIREMENT = "requirement";
|
||||
public static final String REF_TASK = "task";
|
||||
public static final String REF_MASTER = "master";
|
||||
public static final String REF_WAREHOUSE = "warehouse";
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseDetail;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseMaster;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseDetailMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMasterMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 查询某个采购需求关联的入库批次列表。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RequirementBatchService {
|
||||
|
||||
private final SysOaWarehouseMasterMapper masterMapper;
|
||||
private final SysOaWarehouseDetailMapper detailMapper;
|
||||
private final SysOaWarehouseMapper warehouseMapper;
|
||||
|
||||
public List<RequirementBatchVo> listByRequirement(Long requirementId) {
|
||||
// 1. 找所有 type=1 (入库) 且 requirement_id 匹配 的 master
|
||||
List<SysOaWarehouseMaster> masters = masterMapper.selectList(
|
||||
Wrappers.<SysOaWarehouseMaster>lambdaQuery()
|
||||
.eq(SysOaWarehouseMaster::getRequirementId, requirementId)
|
||||
.eq(SysOaWarehouseMaster::getType, 1)
|
||||
.eq(SysOaWarehouseMaster::getDelFlag, 0)
|
||||
.orderByDesc(SysOaWarehouseMaster::getSignTime));
|
||||
if (masters.isEmpty()) return Collections.emptyList();
|
||||
|
||||
// 2. 一次性查这些 master 的 detail
|
||||
List<Long> masterIds = masters.stream()
|
||||
.map(SysOaWarehouseMaster::getMasterId).collect(Collectors.toList());
|
||||
List<SysOaWarehouseDetail> details = detailMapper.selectList(
|
||||
Wrappers.<SysOaWarehouseDetail>lambdaQuery()
|
||||
.in(SysOaWarehouseDetail::getMasterId, masterIds)
|
||||
.eq(SysOaWarehouseDetail::getDelFlag, 0));
|
||||
Map<Long, List<SysOaWarehouseDetail>> detailsByMaster = details.stream()
|
||||
.collect(Collectors.groupingBy(SysOaWarehouseDetail::getMasterId));
|
||||
|
||||
// 3. 一次性查涉及的物料
|
||||
Set<Long> warehouseIds = details.stream()
|
||||
.map(SysOaWarehouseDetail::getWarehouseId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, SysOaWarehouse> whMap = warehouseIds.isEmpty()
|
||||
? Collections.emptyMap()
|
||||
: warehouseMapper.selectBatchIds(warehouseIds).stream()
|
||||
.collect(Collectors.toMap(SysOaWarehouse::getId, w -> w));
|
||||
|
||||
// 4. 组装
|
||||
List<RequirementBatchVo> result = new ArrayList<>();
|
||||
for (SysOaWarehouseMaster m : masters) {
|
||||
RequirementBatchVo vo = new RequirementBatchVo();
|
||||
vo.setMasterId(m.getMasterId());
|
||||
vo.setMasterNum(m.getMasterNum());
|
||||
vo.setSignTime(m.getSignTime());
|
||||
vo.setSignUser(m.getSignUser());
|
||||
|
||||
List<SysOaWarehouseDetail> ds = detailsByMaster.getOrDefault(m.getMasterId(), Collections.emptyList());
|
||||
long totalQty = 0L;
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
List<RequirementBatchVo.BatchDetailVo> dvos = new ArrayList<>();
|
||||
List<String> nameParts = new ArrayList<>();
|
||||
for (SysOaWarehouseDetail d : ds) {
|
||||
SysOaWarehouse w = whMap.get(d.getWarehouseId());
|
||||
long qty = d.getAmount() == null ? 0L : d.getAmount();
|
||||
totalQty += qty;
|
||||
BigDecimal unitPrice = d.getSignPrice() == null ? null : BigDecimal.valueOf(d.getSignPrice());
|
||||
if (unitPrice != null) {
|
||||
totalAmount = totalAmount.add(unitPrice.multiply(BigDecimal.valueOf(qty)));
|
||||
}
|
||||
RequirementBatchVo.BatchDetailVo dv = new RequirementBatchVo.BatchDetailVo();
|
||||
dv.setDetailId(d.getId());
|
||||
dv.setWarehouseId(d.getWarehouseId());
|
||||
dv.setAmount(d.getAmount());
|
||||
dv.setSignPrice(unitPrice);
|
||||
if (w != null) {
|
||||
dv.setWarehouseName(w.getName());
|
||||
dv.setModel(w.getModel());
|
||||
dv.setSpecifications(w.getSpecifications());
|
||||
dv.setUnit(w.getUnit());
|
||||
if (nameParts.size() < 3) nameParts.add(w.getName());
|
||||
}
|
||||
dvos.add(dv);
|
||||
}
|
||||
vo.setDetails(dvos);
|
||||
vo.setTotalQty(totalQty);
|
||||
vo.setTotalAmount(totalAmount);
|
||||
String summary = String.join("、", nameParts);
|
||||
if (dvos.size() > 3) summary += " 等 " + dvos.size() + " 项";
|
||||
vo.setSummary(summary);
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购需求关联的入库批次(一个需求多批到货)
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
public class RequirementBatchVo {
|
||||
|
||||
/** 入库单 ID */
|
||||
private Long masterId;
|
||||
/** 入库单编号 */
|
||||
private String masterNum;
|
||||
/** 入库时间 */
|
||||
private Date signTime;
|
||||
/** 入库人 */
|
||||
private String signUser;
|
||||
/** 本批次物料总件数(detail.amount 之和) */
|
||||
private Long totalQty;
|
||||
/** 本批次金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 物料概览(前几个名字拼接) */
|
||||
private String summary;
|
||||
/** 行明细 */
|
||||
private List<BatchDetailVo> details;
|
||||
|
||||
@Data
|
||||
public static class BatchDetailVo {
|
||||
private Long detailId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String model;
|
||||
private String specifications;
|
||||
private String unit;
|
||||
private Long amount;
|
||||
private BigDecimal signPrice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.oa.audit.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditLog;
|
||||
|
||||
/**
|
||||
* 审计日志 Mapper
|
||||
* 包路径包含 .mapper,被 @MapperScan("com.ruoyi.**.mapper") 自动扫描
|
||||
*/
|
||||
public interface OaWarehouseAuditLogMapper extends BaseMapperPlus<OaWarehouseAuditLogMapper, OaWarehouseAuditLog, OaWarehouseAuditLog> {
|
||||
}
|
||||
@@ -38,6 +38,17 @@ public class OaRequirementsController extends BaseController {
|
||||
|
||||
private final IOaRequirementsService iOaRequirementsService;
|
||||
|
||||
private final com.ruoyi.oa.audit.RequirementBatchService requirementBatchService;
|
||||
|
||||
/**
|
||||
* 查询采购需求关联的入库批次列表
|
||||
*/
|
||||
@GetMapping("/{requirementId}/batches")
|
||||
public R<java.util.List<com.ruoyi.oa.audit.RequirementBatchVo>> batches(
|
||||
@PathVariable Long requirementId) {
|
||||
return R.ok(requirementBatchService.listByRequirement(requirementId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询OA 需求列表
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_oa_warehouse_log")
|
||||
@TableName("_deprecated_sys_oa_warehouse_log")
|
||||
public class SysOaWarehouseLog extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@@ -89,5 +89,8 @@ public class OaRequirementsVo extends BaseEntity {
|
||||
private String ownerNickName;
|
||||
private String projectName;
|
||||
|
||||
/** 附件文件列表(已联查 sys_oss,每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
|
||||
private String accessoryFiles;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -91,4 +91,11 @@ public class SysOaWarehouseMasterVo {
|
||||
private Long requirementId;
|
||||
private String requirementName;
|
||||
|
||||
/** 物料种类数 */
|
||||
private Integer itemCount;
|
||||
/** 入库/出库总数量 */
|
||||
private Long totalQty;
|
||||
/** 物料概览(GROUP_CONCAT 名称) */
|
||||
private String itemsSummary;
|
||||
|
||||
}
|
||||
|
||||
26
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImBind.java
Normal file
26
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImBind.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* OA-OpenIM 用户绑定
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_user_im_bind")
|
||||
public class ImBind {
|
||||
|
||||
@TableId(value = "user_id")
|
||||
private Long userId;
|
||||
|
||||
private String phone;
|
||||
private String imUserId;
|
||||
private Integer bindStatus;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
88
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImSendService.java
Normal file
88
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImSendService.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import com.ruoyi.oa.im.mapper.ImBindMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 业务侧统一推送入口。
|
||||
* 业务代码只关心 OA userId,不用碰 OpenIM 概念。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImSendService {
|
||||
|
||||
private final ImBindMapper bindMapper;
|
||||
private final OpenImClient openImClient;
|
||||
|
||||
/**
|
||||
* 为 OA 用户绑定一个 IM 账号。若 IM 侧已有该手机号则跳过创建;否则先在 chat 注册。
|
||||
* 异步执行,新建/绑定失败不影响业务。
|
||||
*/
|
||||
@Async
|
||||
public void bindOrRegister(Long oaUserId, String phone, String nickname) {
|
||||
if (oaUserId == null || phone == null || phone.isEmpty()) { return; }
|
||||
try {
|
||||
ImBind exist = bindMapper.selectById(oaUserId);
|
||||
if (exist != null && exist.getImUserId() != null) {
|
||||
return;
|
||||
}
|
||||
String imUserId = openImClient.registerImUser(phone, nickname);
|
||||
if (imUserId == null) {
|
||||
log.info("[IM] register skipped for OA user {} phone {}", oaUserId, phone);
|
||||
return;
|
||||
}
|
||||
ImBind bind = new ImBind();
|
||||
bind.setUserId(oaUserId);
|
||||
bind.setPhone(phone);
|
||||
bind.setImUserId(imUserId);
|
||||
bind.setBindStatus(1);
|
||||
bindMapper.insert(bind);
|
||||
log.info("[IM] bound OA {} -> IM {}", oaUserId, imUserId);
|
||||
} catch (Exception e) {
|
||||
log.warn("[IM] bindOrRegister failed for OA {} : {}", oaUserId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给 OA 用户推送一条业务通知(自定义消息)。
|
||||
* 异步执行,失败不抛出(业务流程不应被推送阻塞)。
|
||||
*
|
||||
* @param oaUserId OA 用户ID (sys_user.user_id)
|
||||
* @param title 通知标题
|
||||
* @param description 通知内容(手机端通知栏展示)
|
||||
* @param bizType 业务类型,如 "task" / "project" / "salary"
|
||||
* @param bizId 业务主键,前端点通知后可据此跳转
|
||||
* @param route 前端跳转路径(可选)
|
||||
*/
|
||||
@Async
|
||||
public void sendToOaUser(Long oaUserId, String title, String description,
|
||||
String bizType, Object bizId, String route) {
|
||||
if (oaUserId == null) { return; }
|
||||
try {
|
||||
ImBind bind = bindMapper.selectById(oaUserId);
|
||||
if (bind == null || bind.getImUserId() == null
|
||||
|| (bind.getBindStatus() != null && bind.getBindStatus() == 0)) {
|
||||
log.debug("[IM] user {} not bound, skip push", oaUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("bizType", bizType);
|
||||
payload.put("bizId", bizId);
|
||||
if (route != null) { payload.put("route", route); }
|
||||
|
||||
openImClient.sendCustomToUser(bind.getImUserId(), title, description, payload);
|
||||
} catch (Exception e) {
|
||||
log.warn("[IM] push to OA user {} failed: {}", oaUserId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
220
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImClient.java
Normal file
220
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImClient.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* OpenIM 服务端调用封装
|
||||
* <p>
|
||||
* 1. 管理 admin token(带缓存与过期重取)<br>
|
||||
* 2. 发送自定义消息(OA 业务通知)
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OpenImClient {
|
||||
|
||||
/** 自定义消息 contentType,OpenIM 约定 110 以上为自定义 */
|
||||
public static final int CUSTOM_CONTENT_TYPE = 110;
|
||||
|
||||
/** 会话类型:单聊 */
|
||||
public static final int SESSION_SINGLE = 1;
|
||||
|
||||
private final OpenImProperties props;
|
||||
|
||||
/** 内存缓存的 admin token;过期前重取 */
|
||||
private volatile String adminToken;
|
||||
private volatile long adminTokenExpireAt = 0L;
|
||||
|
||||
/** chat 后端管理员 token 缓存 */
|
||||
private volatile String chatAdminToken;
|
||||
private volatile long chatAdminTokenExpireAt = 0L;
|
||||
|
||||
/** 获取 admin token,自动续期 */
|
||||
public String getAdminToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (adminToken != null && now < adminTokenExpireAt - 60_000L) {
|
||||
return adminToken;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (adminToken != null && now < adminTokenExpireAt - 60_000L) {
|
||||
return adminToken;
|
||||
}
|
||||
Map<String, Object> body = new HashMap<>(2);
|
||||
body.put("secret", props.getSecret());
|
||||
body.put("userID", props.getAdminUserId());
|
||||
|
||||
JSONObject resp = postJson(props.getApiUrl() + "/auth/get_admin_token", body, null);
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
throw new RuntimeException("get admin token failed: " + resp);
|
||||
}
|
||||
adminToken = data.getString("token");
|
||||
long expireSec = data.getLongValue("expireTimeSeconds");
|
||||
if (expireSec <= 0) { expireSec = 7 * 24 * 3600; }
|
||||
adminTokenExpireAt = now + expireSec * 1000L;
|
||||
log.info("[OpenIM] admin token refreshed, expires in {}s", expireSec);
|
||||
return adminToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送自定义业务消息到指定 IM userID(单聊,发送者为 admin)。
|
||||
*
|
||||
* @param recvImUserId 接收者 OpenIM userID
|
||||
* @param title 业务标题(如"任务分派")
|
||||
* @param description 简短描述(通知栏展示)
|
||||
* @param payload 额外业务字段(如 taskId、route 等)
|
||||
* @return true 发送成功
|
||||
*/
|
||||
public boolean sendCustomToUser(String recvImUserId, String title, String description,
|
||||
Map<String, Object> payload) {
|
||||
if (!props.isEnabled()) {
|
||||
log.debug("[OpenIM] disabled, skip send to {}", recvImUserId);
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", title);
|
||||
data.put("description", description);
|
||||
if (payload != null) { data.putAll(payload); }
|
||||
|
||||
Map<String, Object> customElem = new HashMap<>(3);
|
||||
customElem.put("data", JSON.toJSONString(data));
|
||||
customElem.put("description", description);
|
||||
customElem.put("extension", "");
|
||||
|
||||
Map<String, Object> content = new HashMap<>();
|
||||
content.put("customElem", customElem);
|
||||
|
||||
Map<String, Object> offlinePush = new HashMap<>();
|
||||
offlinePush.put("title", title);
|
||||
offlinePush.put("desc", description);
|
||||
offlinePush.put("ex", "");
|
||||
offlinePush.put("iOSPushSound", "default");
|
||||
offlinePush.put("iOSBadgeCount", true);
|
||||
|
||||
Map<String, Object> sendMsg = new HashMap<>();
|
||||
sendMsg.put("sendID", props.getNotificationSender());
|
||||
sendMsg.put("recvID", recvImUserId);
|
||||
sendMsg.put("senderNickname", "系统通知");
|
||||
sendMsg.put("senderPlatformID", 1);
|
||||
sendMsg.put("content", content);
|
||||
sendMsg.put("contentType", CUSTOM_CONTENT_TYPE);
|
||||
sendMsg.put("sessionType", SESSION_SINGLE);
|
||||
sendMsg.put("isOnlineOnly", false);
|
||||
sendMsg.put("notOfflinePush", false);
|
||||
sendMsg.put("offlinePushInfo", offlinePush);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("sendMsg", sendMsg);
|
||||
|
||||
JSONObject resp = postJson(props.getApiUrl() + "/msg/send_msg", body, getAdminToken());
|
||||
Integer errCode = resp.getInteger("errCode");
|
||||
if (errCode != null && errCode == 0) {
|
||||
return true;
|
||||
}
|
||||
log.warn("[OpenIM] send_msg failed: {}", resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** chat 后端 admin token */
|
||||
public String getChatAdminToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (chatAdminToken != null && now < chatAdminTokenExpireAt - 60_000L) {
|
||||
return chatAdminToken;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (chatAdminToken != null && now < chatAdminTokenExpireAt - 60_000L) {
|
||||
return chatAdminToken;
|
||||
}
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("account", props.getChatAdminAccount());
|
||||
body.put("password", md5(props.getChatAdminPassword()));
|
||||
JSONObject resp = postJson(props.getChatAdminUrl() + "/account/login", body, null);
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
if (data == null) { throw new RuntimeException("chat admin login failed: " + resp); }
|
||||
chatAdminToken = data.getString("adminToken");
|
||||
// chat admin token 默认 90 天,保守取 7 天续期
|
||||
chatAdminTokenExpireAt = now + 7L * 24 * 3600_000L;
|
||||
return chatAdminToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 chat 后端创建一个新 IM 账号(无需短信验证)。
|
||||
* 成功返回新 userID,已存在返回 null。
|
||||
*/
|
||||
public String registerImUser(String phoneNumber, String nickname) {
|
||||
if (!props.isEnabled()) { return null; }
|
||||
Map<String, Object> user = new HashMap<>();
|
||||
user.put("nickname", nickname == null ? phoneNumber : nickname);
|
||||
user.put("areaCode", props.getDefaultAreaCode());
|
||||
user.put("phoneNumber", phoneNumber);
|
||||
user.put("password", md5(props.getDefaultUserPassword()));
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("verifyCode", "666666");
|
||||
body.put("deviceID", "oa-backend");
|
||||
body.put("platform", 5);
|
||||
body.put("autoLogin", false);
|
||||
body.put("user", user);
|
||||
|
||||
JSONObject resp = postJson(props.getChatUrl() + "/account/register", body, getChatAdminToken());
|
||||
Integer errCode = resp.getInteger("errCode");
|
||||
if (errCode != null && errCode == 0) {
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
return data == null ? null : data.getString("userID");
|
||||
}
|
||||
if (errCode != null && errCode == 20004) {
|
||||
log.info("[OpenIM] phone {} already registered, skip", phoneNumber);
|
||||
return null;
|
||||
}
|
||||
log.warn("[OpenIM] register failed for phone {}: {}", phoneNumber, resp);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String md5(String text) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) { sb.append(String.format("%02x", b)); }
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("md5 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 内部:POST JSON */
|
||||
private JSONObject postJson(String url, Map<String, Object> body, String token) {
|
||||
HttpRequest req = HttpRequest.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("operationID", UUID.randomUUID().toString())
|
||||
.body(JSON.toJSONString(body))
|
||||
.timeout(8000);
|
||||
if (token != null) { req.header("token", token); }
|
||||
try (HttpResponse resp = req.execute()) {
|
||||
String text = resp.body();
|
||||
if (resp.getStatus() / 100 != 2) {
|
||||
log.warn("[OpenIM] http {} {} -> {}", resp.getStatus(), url, text);
|
||||
}
|
||||
return JSON.parseObject(text);
|
||||
} catch (Exception e) {
|
||||
log.error("[OpenIM] call {} failed", url, e);
|
||||
throw new RuntimeException("openim call failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImProperties.java
Normal file
49
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImProperties.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* OpenIM 配置
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "openim")
|
||||
public class OpenImProperties {
|
||||
|
||||
/** OpenIM 核心 API 地址 */
|
||||
private String apiUrl = "http://49.232.154.205:10002";
|
||||
|
||||
/** OpenIM chat 业务 API 地址 */
|
||||
private String chatUrl = "http://49.232.154.205:10008";
|
||||
|
||||
/** OpenIM chat 管理 API 地址(注册新用户、admin login) */
|
||||
private String chatAdminUrl = "http://49.232.154.205:10009";
|
||||
|
||||
/** chat 后端 admin 账号 */
|
||||
private String chatAdminAccount = "chatAdmin";
|
||||
|
||||
/** chat 后端 admin 密码(md5 后传输;明文配置,登录时自动哈希) */
|
||||
private String chatAdminPassword = "chatAdmin";
|
||||
|
||||
/** 默认区号 */
|
||||
private String defaultAreaCode = "+86";
|
||||
|
||||
/** 新建 IM 用户的默认密码(明文,注册时自动 md5) */
|
||||
private String defaultUserPassword = "123456";
|
||||
|
||||
/** 与 OpenIM share.yml 一致的 secret */
|
||||
private String secret = "openIM123";
|
||||
|
||||
/** 管理员 userID */
|
||||
private String adminUserId = "imAdmin";
|
||||
|
||||
/** 系统通知发送者 userID */
|
||||
private String notificationSender = "imAdmin";
|
||||
|
||||
/** 总开关 */
|
||||
private boolean enabled = true;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.oa.im.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.im.ImBind;
|
||||
|
||||
/**
|
||||
* OA-IM 绑定 Mapper
|
||||
* 包路径包含 .mapper,被 @MapperScan("com.ruoyi.**.mapper") 自动扫描
|
||||
*/
|
||||
public interface ImBindMapper extends BaseMapperPlus<ImBindMapper, ImBind, ImBind> {
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
|
||||
@@ -32,6 +34,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
|
||||
private final OaRequirementsMapper baseMapper;
|
||||
|
||||
private final OaWarehouseAuditService auditService;
|
||||
|
||||
/**
|
||||
* 查询OA 需求
|
||||
*/
|
||||
@@ -85,6 +89,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setRequirementId(add.getRequirementId());
|
||||
auditService.log(OpType.REQ_CREATE, OpType.REF_REQUIREMENT, add.getRequirementId(),
|
||||
"新建采购需求:" + add.getTitle());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -96,7 +102,22 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
public Boolean updateByBo(OaRequirementsBo bo) {
|
||||
OaRequirements update = BeanUtil.toBean(bo, OaRequirements.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean ok = baseMapper.updateById(update) > 0;
|
||||
if (ok) {
|
||||
String op = OpType.REQ_UPDATE;
|
||||
String summary = "修改采购需求:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
if (bo.getStatus() != null) {
|
||||
if (bo.getStatus() == 2) {
|
||||
op = OpType.REQ_DONE;
|
||||
summary = "采购需求完成:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
} else if (bo.getStatus() == 3) {
|
||||
op = OpType.REQ_CANCEL;
|
||||
summary = "采购需求取消:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
}
|
||||
}
|
||||
auditService.log(op, OpType.REF_REQUIREMENT, update.getRequirementId(), summary);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +135,12 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
boolean ok = baseMapper.deleteBatchIds(ids) > 0;
|
||||
if (ok) {
|
||||
for (Long id : ids) {
|
||||
auditService.log(OpType.REQ_DELETE, OpType.REF_REQUIREMENT, id, "删除采购需求");
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.ruoyi.oa.domain.vo.SysOaTaskVo;
|
||||
import com.ruoyi.oa.mapper.SysOaTaskMapper;
|
||||
import com.ruoyi.oa.service.IOaProjectOperationLogService;
|
||||
import com.ruoyi.oa.service.ISysOaTaskService;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -56,6 +57,8 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final ImSendService imSendService;
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
private IOaProjectOperationLogService operationLogService;
|
||||
@@ -217,6 +220,17 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (flag) {
|
||||
operationLogService.recordLog(add.getProjectId(), 3, add.getTaskId(),
|
||||
add.getTaskTitle(), 1, "新增任务: " + add.getTaskTitle(), null, null);
|
||||
// 推送给被分配的执行人(异步,失败不影响业务)
|
||||
if (!Objects.equals(workerId, LoginHelper.getUserId())) {
|
||||
imSendService.sendToOaUser(
|
||||
workerId,
|
||||
"新任务分派",
|
||||
"您有新任务:" + add.getTaskTitle(),
|
||||
"task",
|
||||
add.getTaskId(),
|
||||
"/task/task?taskId=" + add.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
// 判断是否为报工模式
|
||||
if (bo.getStatus()==1L){
|
||||
@@ -307,6 +321,17 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (task != null) {
|
||||
operationLogService.recordLog(task.getProjectId(), 3, bo.getTaskId(),
|
||||
task.getTaskTitle(), 6, "任务申请延期: " + task.getTaskTitle(), null, null);
|
||||
// 推送给任务创建人(审批人)
|
||||
if (task.getCreateUserId() != null
|
||||
&& !Objects.equals(task.getCreateUserId(), LoginHelper.getUserId())) {
|
||||
imSendService.sendToOaUser(
|
||||
task.getCreateUserId(),
|
||||
"任务申请延期",
|
||||
"任务【" + task.getTaskTitle() + "】申请延期,请审批",
|
||||
"task", task.getTaskId(),
|
||||
"/task/task?taskId=" + task.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
@@ -333,6 +358,16 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (ok) {
|
||||
operationLogService.recordLog(sysOaTask.getProjectId(), 3, sysOaTask.getTaskId(),
|
||||
sysOaTask.getTaskTitle(), 7, "任务延期审批通过: " + sysOaTask.getTaskTitle(), null, null);
|
||||
// 推送给执行人,告知延期已批准
|
||||
if (sysOaTask.getWorkerId() != null) {
|
||||
imSendService.sendToOaUser(
|
||||
sysOaTask.getWorkerId(),
|
||||
"任务延期已通过",
|
||||
"任务【" + sysOaTask.getTaskTitle() + "】延期审批已通过",
|
||||
"task", sysOaTask.getTaskId(),
|
||||
"/task/task?taskId=" + sysOaTask.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseDetail;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseLog;
|
||||
import com.ruoyi.oa.domain.bo.ReturnDetailBo;
|
||||
import com.ruoyi.oa.domain.bo.SysOaWarehouseBo;
|
||||
@@ -59,6 +61,9 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
@Autowired
|
||||
private SysOaWarehouseLogMapper warehouseLogMapper;
|
||||
|
||||
@Autowired
|
||||
private OaWarehouseAuditService auditService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询出库单管理
|
||||
@@ -117,6 +122,10 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
lqw.eq(bo.getType() != null, "sowm.type", bo.getType())
|
||||
.eq(bo.getProjectId()!= null, "sowm.project_id", bo.getProjectId())
|
||||
.eq(bo.getReturnType() != null, "sowm.return_type", bo.getReturnType())
|
||||
.eq(bo.getStatus() != null, "sowm.status", bo.getStatus())
|
||||
.eq(bo.getRequirementId() != null, "sowm.requirement_id", bo.getRequirementId())
|
||||
.like(bo.getMasterNum() != null && !bo.getMasterNum().isEmpty(), "sowm.master_num", bo.getMasterNum())
|
||||
.like(bo.getRemark() != null && !bo.getRemark().isEmpty(), "sowm.remark", bo.getRemark())
|
||||
// 其他过滤……
|
||||
.eq("sowm.del_flag", 0)
|
||||
.orderByDesc("sowm.create_time");
|
||||
@@ -154,6 +163,20 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
public Boolean insertByBo(SysOaWarehouseMasterBo bo) {
|
||||
SysOaWarehouseMaster add = BeanUtil.toBean(bo, SysOaWarehouseMaster.class);
|
||||
validEntityBeforeSave(add);
|
||||
|
||||
// 规则 1:出库(type=0)无项目 + 无备注 → 自动写入"办公耗材自动写入"
|
||||
if (add.getType() != null && add.getType() == 0
|
||||
&& add.getProjectId() == null
|
||||
&& (add.getRemark() == null || add.getRemark().trim().isEmpty())) {
|
||||
add.setRemark("办公耗材自动写入");
|
||||
}
|
||||
|
||||
// 规则 2:入库(type=1)必须强关联采购需求(requirement_id)
|
||||
if (add.getType() != null && add.getType() == 1
|
||||
&& add.getRequirementId() == null) {
|
||||
throw new RuntimeException("入库必须关联采购需求,请先选择需求");
|
||||
}
|
||||
|
||||
add.setReturnType(0); // 默认0
|
||||
add.setSignUser(LoginHelper.getNickName());
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
@@ -164,6 +187,16 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
sysOaWarehouseDetailBo.setType(bo.getType());
|
||||
warehouseDetailService.insertByBo(sysOaWarehouseDetailBo);
|
||||
}
|
||||
// 审计:根据 type 写不同操作类型
|
||||
String op = add.getType() == 0 ? OpType.OUT
|
||||
: add.getType() == 1 ? OpType.IN
|
||||
: OpType.RETURN;
|
||||
String typeLabel = add.getType() == 0 ? "出库" : add.getType() == 1 ? "入库" : "退库";
|
||||
auditService.log(op, OpType.REF_MASTER, add.getMasterId(),
|
||||
add.getProjectId(), null,
|
||||
typeLabel + "单 " + (add.getMasterNum() == null ? "" : add.getMasterNum())
|
||||
+ ",共 " + (bo.getWarehouseList() == null ? 0 : bo.getWarehouseList().size()) + " 项",
|
||||
null, null);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -211,13 +244,11 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
SysOaWarehouse warehouse = warehouseMapper.selectById(detail.getWarehouseId());
|
||||
warehouse.setInventory(warehouse.getInventory() + dto.getReturnNum());
|
||||
warehouseMapper.updateById(warehouse);
|
||||
// 日志表插入记录
|
||||
SysOaWarehouseLog log = new SysOaWarehouseLog();
|
||||
log.setMasterId(detail.getMasterId());
|
||||
log.setWarehouseId(detail.getWarehouseId());
|
||||
log.setNum(Long.valueOf(dto.getReturnNum()));
|
||||
log.setRemark("退库操作");
|
||||
warehouseLogMapper.insert(log);
|
||||
// 统一审计日志
|
||||
auditService.log(OpType.RETURN, OpType.REF_MASTER, detail.getMasterId(),
|
||||
null, detail.getWarehouseId(),
|
||||
"退库 " + dto.getReturnNum() + " 件,物料 " + warehouse.getName(),
|
||||
null, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMasterMapper;
|
||||
import com.ruoyi.oa.service.ISysOaWarehouseDetailService;
|
||||
import com.ruoyi.oa.service.ISysOaWarehouseMasterService;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -43,6 +45,8 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
|
||||
private final SysOaWarehouseTaskMapper baseMapper;
|
||||
|
||||
private final OaWarehouseAuditService auditService;
|
||||
|
||||
private final SysOaWarehouseMasterMapper masterMapper;
|
||||
|
||||
@Autowired
|
||||
@@ -100,6 +104,11 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setTaskId(add.getTaskId());
|
||||
auditService.log(OpType.TASK_CREATE, OpType.REF_TASK, add.getTaskId(),
|
||||
null, add.getWarehouseId(),
|
||||
"新建车间采购:" + (add.getName() == null ? "" : add.getName())
|
||||
+ " × " + (add.getTaskInventory() == null ? 0 : add.getTaskInventory()),
|
||||
null, null);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -111,7 +120,19 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
public Boolean updateByBo(SysOaWarehouseTaskBo bo) {
|
||||
SysOaWarehouseTask update = BeanUtil.toBean(bo, SysOaWarehouseTask.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean ok = baseMapper.updateById(update) > 0;
|
||||
if (ok && bo.getTaskStatus() != null) {
|
||||
String op;
|
||||
String label;
|
||||
if (bo.getTaskStatus() == 2) { op = OpType.TASK_DONE; label = "完成"; }
|
||||
else if (bo.getTaskStatus() == 3) { op = OpType.TASK_CANCEL; label = "取消"; }
|
||||
else { op = "TASK_UPDATE"; label = "修改"; }
|
||||
auditService.log(op, OpType.REF_TASK, update.getTaskId(),
|
||||
null, update.getWarehouseId(),
|
||||
label + "车间采购:" + (update.getName() == null ? "" : update.getName()),
|
||||
null, null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.OaSalaryMaster;
|
||||
import com.ruoyi.oa.domain.SysOaRemind;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import com.ruoyi.oa.mapper.OaSalaryMasterMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaRemindMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -19,6 +20,7 @@ public class OaSalaryRemindScheduler {
|
||||
|
||||
private final OaSalaryMasterMapper salaryMasterMapper;
|
||||
private final SysOaRemindMapper remindMapper;
|
||||
private final ImSendService imSendService;
|
||||
|
||||
// 老板用户ID
|
||||
private static final Long BOSS_USER_ID = 1859252208375152641L;
|
||||
@@ -60,6 +62,15 @@ public class OaSalaryRemindScheduler {
|
||||
remind.setRemark(master.getRemark());
|
||||
|
||||
remindMapper.insert(remind);
|
||||
|
||||
// 同时通过 IM 推送给老板
|
||||
imSendService.sendToOaUser(
|
||||
BOSS_USER_ID,
|
||||
"工资审批提醒",
|
||||
remind.getContent(),
|
||||
"salary", master.getMasterId(),
|
||||
"/finance/salary/list"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ruoyi.oa.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.oa.domain.SysOaProject;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import com.ruoyi.oa.mapper.SysOaProjectMapper;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目到期提醒:每天 09:00 扫描 finishTime 在 [today, today+THRESHOLD_DAYS] 区间且未完结的项目,
|
||||
* 通过 OpenIM 推送给项目负责人。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectExpiryRemindScheduler {
|
||||
|
||||
/** 提前多少天提醒 */
|
||||
private static final int THRESHOLD_DAYS = 3;
|
||||
|
||||
private final SysOaProjectMapper projectMapper;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ImSendService imSendService;
|
||||
|
||||
@Scheduled(cron = "0 0 9 * * ?")
|
||||
public void notifyExpiringProjects() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
|
||||
Date start = cal.getTime();
|
||||
cal.add(Calendar.DAY_OF_MONTH, THRESHOLD_DAYS + 1);
|
||||
Date end = cal.getTime();
|
||||
|
||||
LambdaQueryWrapper<SysOaProject> qw = Wrappers.lambdaQuery();
|
||||
qw.between(SysOaProject::getFinishTime, start, end);
|
||||
// 已结项的不推("3" 通常表示已完结,按你们的字典调整)
|
||||
qw.ne(SysOaProject::getProjectStatus, "3");
|
||||
List<SysOaProject> projects = projectMapper.selectList(qw);
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (SysOaProject p : projects) {
|
||||
String nickName = p.getFunctionary();
|
||||
if (nickName == null || nickName.isEmpty()) { continue; }
|
||||
|
||||
// functionary 存的是 nickName,反查 user_id
|
||||
SysUser user = userMapper.selectOne(
|
||||
Wrappers.<SysUser>lambdaQuery()
|
||||
.eq(SysUser::getNickName, nickName)
|
||||
.eq(SysUser::getDelFlag, "0")
|
||||
.last("limit 1")
|
||||
);
|
||||
if (user == null) {
|
||||
log.debug("[ProjectExpiry] nickName={} 未找到对应用户,跳过", nickName);
|
||||
continue;
|
||||
}
|
||||
|
||||
String title = "项目即将到期";
|
||||
String desc = String.format("项目【%s】将于 %s 到期,请及时跟进",
|
||||
p.getProjectName(), sdf.format(p.getFinishTime()));
|
||||
|
||||
imSendService.sendToOaUser(
|
||||
user.getUserId(), title, desc,
|
||||
"project", p.getProjectId(),
|
||||
"/oa/project?projectId=" + p.getProjectId()
|
||||
);
|
||||
}
|
||||
log.info("[ProjectExpiry] 推送 {} 个即将到期项目", projects.size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user