推送项目重构代码

This commit is contained in:
2026-05-30 15:32:57 +08:00
parent 3dafaceef2
commit a28ea44cab
53 changed files with 3525 additions and 731 deletions

View File

@@ -24,6 +24,10 @@ import com.ruoyi.oa.domain.vo.OaProjectScheduleVo;
import com.ruoyi.oa.domain.bo.OaProjectScheduleBo;
import com.ruoyi.oa.service.IOaProjectScheduleService;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.oa.im.ImSendService;
import com.ruoyi.system.service.ISysUserService;
/**
* 项目进度
@@ -38,6 +42,43 @@ import com.ruoyi.common.core.page.TableDataInfo;
public class OaProjectScheduleController extends BaseController {
private final IOaProjectScheduleService iOaProjectScheduleService;
private final ImSendService imSendService;
private final ISysUserService userService;
/**
* 催促进度:给项目负责人发一条 IM 消息
*/
@PostMapping("/urge/{scheduleId}")
public R<Void> urgeProgress(@PathVariable Long scheduleId) {
OaProjectScheduleVo vo = iOaProjectScheduleService.queryById(scheduleId);
if (vo == null) {
return R.fail("项目进度不存在");
}
String header = vo.getHeader();
if (header == null || header.isEmpty()) {
return R.fail("该项目未设置负责人");
}
SysUser user = userService.selectUserByNickName(header);
if (user == null) {
return R.fail("找不到负责人 " + header + " 的账号");
}
// 防止自己催自己 → 还是发,但提示
String me = LoginHelper.getNickName();
String projectName = vo.getProjectName() == null ? "(未命名项目)" : vo.getProjectName();
String currentStep = vo.getCurrentStepName() == null ? "(无)" : vo.getCurrentStepName();
int percent = vo.getSchedulePercentage() == null ? 0 : vo.getSchedulePercentage().intValue();
long delay = vo.getDelayCount() == null ? 0 : vo.getDelayCount();
String title = "进度催促";
String desc = String.format("[%s] 催促您推进【%s】当前 %d%%,当前步骤:%s%s",
me == null ? "系统" : me, projectName, percent, currentStep,
delay > 0 ? ",已有 " + delay + " 步延期" : "");
imSendService.sendToOaUser(user.getUserId(), title, desc,
"schedule", scheduleId, "/projectSchedule?scheduleId=" + scheduleId);
return R.ok();
}
/**
* 查询项目进度列表

View File

@@ -57,6 +57,9 @@ public class SysOaWarehouseMaster extends BaseEntity {
*/
private String remark;
/** 收货单/相关附件 OSS ID逗号分隔 */
private String receiptDoc;
private Integer isLike;
private Long status;
private Integer returnType;

View File

@@ -61,6 +61,9 @@ public class SysOaWarehouseMasterBo extends BaseEntity {
*/
private String remark;
/** 收货单/相关附件 OSS ID逗号分隔 */
private String receiptDoc;
/**
* 涉及物料
*/

View File

@@ -97,5 +97,9 @@ public class SysOaWarehouseMasterVo {
private Long totalQty;
/** 物料概览GROUP_CONCAT 名称) */
private String itemsSummary;
/** 收货单 OSS IDCSV */
private String receiptDoc;
/** 收货单文件信息(已联查 sys_oss格式 ossId|name|url,, */
private String receiptFiles;
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.oa.im;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.oa.im.mapper.ImBindMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 当前用户的 IM 凭据(供 Web SDK 登录)
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/user/im")
public class ImCredentialsController extends BaseController {
private final ImBindMapper bindMapper;
private final OpenImClient openImClient;
private final OpenImProperties props;
/** 当前用户 IM 登录所需信息imUserId + token + apiUrl + wsUrl */
@GetMapping("/credentials")
public R<Map<String, Object>> credentials() {
Long userId = LoginHelper.getUserId();
ImBind bind = bindMapper.selectById(userId);
if (bind == null || bind.getImUserId() == null) {
return R.fail("当前账号未绑定 IM");
}
// platformId=5: Web
String token = openImClient.issueUserToken(bind.getImUserId(), 5);
if (token == null) {
return R.fail("获取 IM token 失败");
}
Map<String, Object> data = new HashMap<>();
data.put("imUserId", bind.getImUserId());
data.put("imToken", token);
data.put("apiUrl", props.getPublicApiUrl());
data.put("wsUrl", props.getWsUrl());
return R.ok(data);
}
}

View File

@@ -106,20 +106,21 @@ public class OpenImClient {
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);
// SendMsg 字段需要嵌套在 sendMessage 对象里OpenIM v3.8 约定)
Map<String, Object> sendMessage = new HashMap<>();
sendMessage.put("sendID", props.getNotificationSender());
sendMessage.put("recvID", recvImUserId);
sendMessage.put("senderNickname", "系统通知");
sendMessage.put("senderPlatformID", 1);
sendMessage.put("content", content);
sendMessage.put("contentType", CUSTOM_CONTENT_TYPE);
sendMessage.put("sessionType", SESSION_SINGLE);
sendMessage.put("isOnlineOnly", false);
sendMessage.put("notOfflinePush", false);
sendMessage.put("offlinePushInfo", offlinePush);
Map<String, Object> body = new HashMap<>();
body.put("sendMsg", sendMsg);
body.put("sendMessage", sendMessage);
JSONObject resp = postJson(props.getApiUrl() + "/msg/send_msg", body, getAdminToken());
Integer errCode = resp.getInteger("errCode");
@@ -130,6 +131,26 @@ public class OpenImClient {
return false;
}
/**
* 用 admin 身份签发 IM 用户 tokenWeb SDK 登录需要)。
* @param imUserId IM userID
* @param platformId 平台编号5=Web
*/
public String issueUserToken(String imUserId, int platformId) {
if (!props.isEnabled()) { return null; }
Map<String, Object> body = new HashMap<>();
body.put("secret", props.getSecret());
body.put("platformID", platformId);
body.put("userID", imUserId);
JSONObject resp = postJson(props.getApiUrl() + "/auth/get_user_token", body, getAdminToken());
JSONObject data = resp.getJSONObject("data");
if (data == null) {
log.warn("[OpenIM] issueUserToken failed: {}", resp);
return null;
}
return data.getString("token");
}
/** chat 后端 admin token */
public String getChatAdminToken() {
long now = System.currentTimeMillis();

View File

@@ -14,14 +14,20 @@ import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "openim")
public class OpenImProperties {
/** OpenIM 核心 API 地址 */
private String apiUrl = "http://49.232.154.205:10002";
/** OpenIM 核心 API 地址(走 nginx 网关,:10002 直连端口外网未开放) */
private String apiUrl = "http://49.232.154.205:10006/api";
/** OpenIM chat 业务 API 地址 */
private String chatUrl = "http://49.232.154.205:10008";
/** OpenIM WS 长连接地址(前端 SDK 用) */
private String wsUrl = "ws://49.232.154.205:10006/msg_gateway";
/** OpenIM chat 管理 API 地址注册新用户、admin login */
private String chatAdminUrl = "http://49.232.154.205:10009";
/** 前端 SDK 可以直接访问的 API 地址(如果跟后端调用地址不同 */
private String publicApiUrl = "http://49.232.154.205:10006/api";
/** OpenIM chat 业务 API 地址(走 nginx 网关) */
private String chatUrl = "http://49.232.154.205:10006/chat";
/** OpenIM chat 管理 API 地址(走 nginx 网关) */
private String chatAdminUrl = "http://49.232.154.205:10006/chat";
/** chat 后端 admin 账号 */
private String chatAdminAccount = "chatAdmin";

View File

@@ -1,5 +1,6 @@
package com.ruoyi.oa.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -18,5 +19,7 @@ import java.util.Map;
*/
public interface OaProjectScheduleMapper extends BaseMapperPlus<OaProjectScheduleMapper, OaProjectSchedule, OaProjectScheduleVo> {
/** dataPermission="true" 让 PlusDataPermissionInterceptor 跳过解析SQL 太复杂 JsqlParser 不识别) */
@InterceptorIgnore(dataPermission = "true")
Page<OaProjectScheduleVo> selectVoPagePlus(@Param("page") Page<OaProjectScheduleVo> build,@Param(Constants.WRAPPER) QueryWrapper<OaProjectSchedule> lqw);
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.oa.suggestion;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 用户修改意见(区别于已有的 oa_feedback 问题反馈,本表为顶栏入口提交的改进意见)
*/
@Data
@TableName("sys_oa_feedback")
public class UserSuggestion {
@TableId(value = "feedback_id")
private Long feedbackId;
private String title;
private String content;
private String category;
private Integer priority;
private String pagePath;
private String attachment;
private Long submitterId;
private String submitterName;
/** 0待处理 1已受理 2已完成 3已关闭 */
private Integer status;
private Long handlerId;
private String handlerName;
private String acceptRemark;
private Date acceptTime;
private Date finishTime;
private String createBy;
private Date createTime;
private String updateBy;
private Date updateTime;
private Integer delFlag;
}

View File

@@ -0,0 +1,148 @@
package com.ruoyi.oa.suggestion;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.oa.im.ImSendService;
import com.ruoyi.oa.suggestion.mapper.UserSuggestionMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
/**
* 用户修改意见
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/oa/suggestion")
public class UserSuggestionController extends BaseController {
/** 信息化部 dept_id */
private static final Long IT_DEPT_ID = 1858416909042524161L;
private final UserSuggestionMapper mapper;
private final ImSendService imSendService;
private final SysUserMapper userMapper;
@GetMapping("/list")
public TableDataInfo<UserSuggestion> list(UserSuggestion query, PageQuery pageQuery) {
LambdaQueryWrapper<UserSuggestion> qw = Wrappers.lambdaQuery();
qw.eq(UserSuggestion::getDelFlag, 0);
Long uid = LoginHelper.getUserId();
if (!isItDeptMember(uid)) {
qw.eq(UserSuggestion::getSubmitterId, uid);
}
if (query.getStatus() != null) qw.eq(UserSuggestion::getStatus, query.getStatus());
if (query.getCategory() != null && !query.getCategory().isEmpty()) {
qw.eq(UserSuggestion::getCategory, query.getCategory());
}
if (query.getTitle() != null && !query.getTitle().isEmpty()) {
qw.like(UserSuggestion::getTitle, query.getTitle());
}
qw.orderByDesc(UserSuggestion::getCreateTime);
Page<UserSuggestion> result = mapper.selectPage(pageQuery.build(), qw);
return TableDataInfo.build(result);
}
@PostMapping
public R<Void> submit(@RequestBody UserSuggestion body) {
Long uid = LoginHelper.getUserId();
String name = LoginHelper.getNickName();
body.setFeedbackId(null);
body.setSubmitterId(uid);
body.setSubmitterName(name);
body.setStatus(0);
body.setCreateBy(name);
body.setCreateTime(new Date());
body.setDelFlag(0);
mapper.insert(body);
List<SysUser> itUsers = userMapper.selectList(
Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getDeptId, IT_DEPT_ID)
.eq(SysUser::getDelFlag, "0"));
String title = "新的修改意见";
String desc = String.format("[%s] %s", name == null ? "用户" : name,
body.getTitle() == null ? "未命名" : body.getTitle());
for (SysUser u : itUsers) {
if (u.getUserId() == null || u.getUserId().equals(uid)) continue;
imSendService.sendToOaUser(u.getUserId(), title, desc,
"suggestion", body.getFeedbackId(),
"/system/feedback?id=" + body.getFeedbackId());
}
return R.ok();
}
@PutMapping("/{id}/accept")
public R<Void> accept(@PathVariable Long id, @RequestParam(required = false) String remark) {
UserSuggestion fb = mapper.selectById(id);
if (fb == null) return R.fail("不存在");
if (!isItDeptMember(LoginHelper.getUserId())) return R.fail("仅信息化部可操作");
fb.setStatus(1);
fb.setHandlerId(LoginHelper.getUserId());
fb.setHandlerName(LoginHelper.getNickName());
fb.setAcceptRemark(remark);
fb.setAcceptTime(new Date());
mapper.updateById(fb);
if (fb.getSubmitterId() != null) {
imSendService.sendToOaUser(fb.getSubmitterId(),
"您的意见已受理",
String.format("[%s] 受理了您的意见:%s%s",
fb.getHandlerName() == null ? "信息化部" : fb.getHandlerName(),
fb.getTitle(),
remark == null || remark.isEmpty() ? "" : "(备注:" + remark + ""),
"suggestion", id, "/system/feedback?id=" + id);
}
return R.ok();
}
@PutMapping("/{id}/finish")
public R<Void> finish(@PathVariable Long id, @RequestParam(required = false) String remark) {
UserSuggestion fb = mapper.selectById(id);
if (fb == null) return R.fail("不存在");
if (!isItDeptMember(LoginHelper.getUserId())) return R.fail("仅信息化部可操作");
fb.setStatus(2);
fb.setFinishTime(new Date());
if (remark != null) fb.setAcceptRemark(remark);
mapper.updateById(fb);
if (fb.getSubmitterId() != null) {
imSendService.sendToOaUser(fb.getSubmitterId(),
"您的意见已完成",
String.format("[%s] 已处理完毕:%s", fb.getHandlerName(), fb.getTitle()),
"suggestion", id, "/system/feedback?id=" + id);
}
return R.ok();
}
@PutMapping("/{id}/close")
public R<Void> close(@PathVariable Long id, @RequestParam(required = false) String remark) {
UserSuggestion fb = mapper.selectById(id);
if (fb == null) return R.fail("不存在");
Long uid = LoginHelper.getUserId();
if (!isItDeptMember(uid) && !uid.equals(fb.getSubmitterId())) return R.fail("无权关闭");
fb.setStatus(3);
if (remark != null) fb.setAcceptRemark(remark);
mapper.updateById(fb);
return R.ok();
}
@GetMapping("/isItMember")
public R<Boolean> isItMember() {
return R.ok(isItDeptMember(LoginHelper.getUserId()));
}
private boolean isItDeptMember(Long userId) {
if (userId == null) return false;
SysUser u = userMapper.selectById(userId);
return u != null && IT_DEPT_ID.equals(u.getDeptId());
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.oa.suggestion.mapper;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.oa.suggestion.UserSuggestion;
public interface UserSuggestionMapper extends BaseMapperPlus<UserSuggestionMapper, UserSuggestion, UserSuggestion> {
}

View File

@@ -22,6 +22,13 @@
</resultMap>
<select id="selectVoPagePlus_COUNT" resultType="java.lang.Long">
SELECT COUNT(*)
FROM oa_project_schedule AS ops
LEFT JOIN sys_oa_project AS op ON ops.project_id = op.project_id
${ew.getCustomSqlSegment}
</select>
<select id="selectVoPagePlus" resultType="com.ruoyi.oa.domain.vo.OaProjectScheduleVo">
SELECT ops.schedule_id,
ops.project_id,
@@ -32,7 +39,6 @@
ops.status,
ops.steward,
ops.remark,
/* ======================== 项目信息 ==================== */
op.project_id AS opProjectId,
op.project_name,
op.project_num,
@@ -48,22 +54,33 @@
op.trade_type,
op.project_code,
op.pre_pay,
op.is_top AS isTop,
(SELECT COUNT(*) FROM oa_project_schedule_step opss
WHERE opss.schedule_id = ops.schedule_id AND opss.del_flag = '0') AS totalCount,
(SELECT COUNT(*) FROM oa_project_schedule_step opss
WHERE opss.schedule_id = ops.schedule_id AND opss.del_flag = '0' AND opss.status IN (0,1)) AS unFinishCount,
(SELECT COUNT(*) FROM oa_project_schedule_step opss
WHERE opss.schedule_id = ops.schedule_id AND opss.del_flag = '0'
AND (
opss.use_flag = 0
OR (opss.original_end_time IS NOT NULL AND opss.plan_end IS NOT NULL AND opss.plan_end > opss.original_end_time)
OR (opss.status = 0 AND opss.original_end_time IS NOT NULL AND CURDATE() > DATE(opss.original_end_time))
)
) AS delayCount
op.is_top AS isTop,
op.functionary AS header,
cs.step_name AS currentStepName,
IFNULL(agg.total_count, 0) AS totalCount,
IFNULL(agg.unfinish_count, 0) AS unFinishCount,
IFNULL(agg.delay_count, 0) AS delayCount,
IFNULL(agg.percent, 0) AS schedulePercentage
FROM oa_project_schedule AS ops
LEFT JOIN sys_oa_project AS op
ON ops.project_id = op.project_id
LEFT JOIN oa_project_schedule_step cs
ON cs.track_id = ops.current_step
LEFT JOIN (
SELECT
opss.schedule_id,
COUNT(*) AS total_count,
SUM(CASE WHEN opss.status IN (0,1) THEN 1 ELSE 0 END) AS unfinish_count,
SUM(CASE
WHEN opss.use_flag = 0
OR (opss.original_end_time IS NOT NULL AND opss.plan_end IS NOT NULL AND opss.plan_end > opss.original_end_time)
OR (opss.status = 0 AND opss.original_end_time IS NOT NULL AND CURDATE() > DATE(opss.original_end_time))
THEN 1 ELSE 0 END) AS delay_count,
ROUND(SUM(CASE WHEN opss.status = 2 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS percent
FROM oa_project_schedule_step opss
WHERE opss.del_flag = '0'
GROUP BY opss.schedule_id
) AS agg ON agg.schedule_id = ops.schedule_id
${ew.getCustomSqlSegment}
</select>

View File

@@ -32,7 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SELECT GROUP_CONCAT(CONCAT_WS('|', o.oss_id, COALESCE(o.original_name, o.file_name), o.url)
ORDER BY FIND_IN_SET(o.oss_id, r.accessory) SEPARATOR ',,')
FROM sys_oss o
WHERE r.accessory IS NOT NULL AND r.accessory <> ''
WHERE r.accessory IS NOT NULL AND r.accessory != ''
AND FIND_IN_SET(o.oss_id, r.accessory)
) AS accessory_files
FROM oa_requirements r
@@ -52,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SELECT GROUP_CONCAT(CONCAT_WS('|', o.oss_id, COALESCE(o.original_name, o.file_name), o.url)
ORDER BY FIND_IN_SET(o.oss_id, r.accessory) SEPARATOR ',,')
FROM sys_oss o
WHERE r.accessory IS NOT NULL AND r.accessory <> ''
WHERE r.accessory IS NOT NULL AND r.accessory != ''
AND FIND_IN_SET(o.oss_id, r.accessory)
) AS accessory_files
FROM oa_requirements r

View File

@@ -29,6 +29,8 @@
<result property="itemCount" column="item_count"/>
<result property="totalQty" column="total_qty"/>
<result property="itemsSummary" column="items_summary"/>
<result property="receiptDoc" column="receipt_doc"/>
<result property="receiptFiles" column="receipt_files"/>
<collection property="warehouseList"
column="master_id"
@@ -56,6 +58,7 @@
sowm.sign_time,
sowm.sign_user,
sowm.remark,
sowm.receipt_doc,
sowm.status,
sowm.is_like,
sowm.requirement_id,
@@ -64,7 +67,14 @@
req.title AS requirementName,
agg.item_count,
agg.total_qty,
agg.items_summary
agg.items_summary,
(
SELECT GROUP_CONCAT(CONCAT_WS('|', o.oss_id, COALESCE(o.original_name, o.file_name), o.url)
ORDER BY FIND_IN_SET(o.oss_id, sowm.receipt_doc) SEPARATOR ',,')
FROM sys_oss o
WHERE sowm.receipt_doc IS NOT NULL AND sowm.receipt_doc != ''
AND FIND_IN_SET(o.oss_id, sowm.receipt_doc)
) AS receipt_files
FROM sys_oa_warehouse_master sowm
LEFT JOIN sys_oa_project sop ON sop.project_id = sowm.project_id
LEFT JOIN oa_requirements req ON req.requirement_id = sowm.requirement_id