Compare commits

...

54 Commits

Author SHA1 Message Date
47baa575df 推送任务进度操作历史,推送项目总览 2026-05-10 16:38:39 +08:00
9ce5cb8f2e 修改配置 2026-05-08 20:36:37 +08:00
ccf87c06ff Merge: OCR失败降级手动填写 + 手动新增条目 2026-05-08 19:49:06 +08:00
156602fd59 feat(报销/拨款): OCR失败时保留空条目供手动填写,支持手动新增条目和行内附件上传
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:49:03 +08:00
2752a31a49 修改配置 2026-05-08 18:55:53 +08:00
40fdd14d13 feat(报销/拨款): 进入页面检测OCR服务状态
- 后端新增 GET /ocr-health 端点,探测 Python OCR 服务 /health
- 前端页面 created 时调用健康检查,服务不可用时顶部显示红色警告
  "发票识别服务已停止,请联系信息化部门"
- 服务不可用时禁用附件上传区域(FileUpload 新增 disabled prop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:47:51 +08:00
f1158d1e16 Merge: fix ossId精度丢失 2026-05-08 18:32:09 +08:00
6055f06f83 fix(报销/拨款): 修复ossId精度丢失导致附件下载失败
Number() 会将超出 JS 安全整数范围的雪花ID末位截断,
改为直接保留后端返回的字符串,保持精度。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:32:09 +08:00
a0d8f459e4 Merge: feat(详情) 发票明细内联附件下载 2026-05-08 18:28:19 +08:00
5672b1c07a feat(报销/拨款详情): 发票明细内联附件下载,移除独立单据区块
- 发票明细表格新增「附件」列,同一文件只在首行显示下载按钮
- 移除独立的「报销/拨款单据」区块,文件通过明细行直接下载
- 无发票明细的老记录保留附件兜底展示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:28:19 +08:00
89f47860a7 Merge: fix(报销/拨款) 行项目金额改为价税合计,移除总金额输入框 2026-05-08 18:22:07 +08:00
7f9ae18022 fix(报销/拨款): 行项目金额改为价税合计,移除总金额手动输入框
- OCR解析:行项目金额改为 amount + tax_amount(价税合计)
- 总金额改为只读展示,由明细汇总自动计算,不再支持手动输入
- 去掉总金额字段的表单必填校验

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:22:04 +08:00
f5b2ddb743 Merge: feat(报销/拨款) 发票明细与附件双向联动 2026-05-08 17:49:47 +08:00
1e128cecfe feat(报销/拨款): 发票明细与附件双向联动
- 移除手动添加条目按钮,条目只能通过上传文件产生
- 删除附件时同步移除该文件的所有明细条目
- 删除条目时同步从附件列表移除对应文件(含该文件下全部条目)
- 无明细时隐藏明细表格区域

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:49:43 +08:00
534a64a874 Merge branch 'claude/magical-chebyshev-fe4d6c': feat(报销/拨款) 新增发票明细子表与OCR自动识别 2026-05-08 17:34:18 +08:00
c412f73b80 feat(报销/拨款): 新增发票明细子表与OCR自动识别
- 新增 hrm_invoice_item 共享子表(biz_type区分报销/拨款),每条记录对应一张发票条目
- 新增 HrmInvoiceOcrService,上传附件后自动调用 ai-ocr Python服务识别发票,结果逐条回填表单
- 报销/拨款申请提交及更新时同步保存发票明细;queryById 返回关联发票条目列表
- 前端:附件上传后自动触发OCR,展示"模型思考中"状态,识别完成后自动填充金额
- 详情页新增发票明细只读表格展示,兼容无明细的历史记录
- application.yml 增加 fad.ocr 配置项

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:34:08 +08:00
28a37f4105 feat(报销页面): 提高附件上传数量限制至200个
将报销单据附件和回执附件的上传数量限制从50个和10个统一提高到200个,以满足用户上传更多附件的需求
2026-05-07 10:31:54 +08:00
975c57f12b Merge branch 'main' of http://49.232.154.205:10100/DeXun/fad_oa 2026-05-07 10:29:31 +08:00
67d4519462 feat(需求管理): 在需求列表中添加创建日期列
添加创建日期列以显示需求的创建时间,格式化为年-月-日,与截止日期列保持一致
2026-05-07 10:29:07 +08:00
dc67788f51 完成ocr识别发票的前提条件 2026-05-05 20:15:54 +08:00
04c84a3ed3 报工添加了手动录入能力 2026-05-05 19:49:11 +08:00
acf0048bf1 采购需求添加开始时间列 2026-04-29 13:58:29 +08:00
2ac3901f75 feat(hrm): 添加流程抄送标记已读未读功能
- 扩展操作列宽度从100到150以适应新功能
- 添加标记未读链接在已读状态下显示
- 实现handleUnread方法用于取消已读状态
- 优化表格操作按钮布局结构
2026-04-27 14:42:01 +08:00
3613b6d83a refactor(oa): 需求管理模块的Vo修改
- 继承 BaseEntity 类以统一实体基类功能
- 添加 BaseEntity 依赖导入以支持基础属性扩展
- 保持现有数据传输对象结构不变
2026-04-25 16:11:58 +08:00
4faad94c79 feat(oa): 添加定位权限说明提示功能
- 添加了定位获取失败时的操作指引提示框
- 提供了浏览器定位权限配置的详细步骤说明
- 包含Edge和Chrome浏览器的配置方法
- 添加了站点地址的配置示例
- 更新了工作地点输入框的提示文字格式
2026-04-25 14:29:38 +08:00
f6b5940a27 merge origin main conflicts resolved 2026-04-23 13:20:29 +08:00
db90e2a084 feat: 添加项目进度统计功能,支持在列表中显示各项目的进度步骤统计信息,以及跳转 2026-04-23 12:47:23 +08:00
f48818b14d Merge remote-tracking branch 'origin/main'
# Conflicts:
#	ruoyi-ui/src/views/oa/project/dashboard2/index.vue
#	ruoyi-ui/src/views/oa/project/pace/components/xmind.vue
2026-04-22 18:45:45 +08:00
602928dc0b 修复综合看板 2026-04-22 18:44:14 +08:00
455f3bbf09 Merge branch 'main' of http://49.232.154.205:10100/liujingchao/fad_oa 2026-04-22 18:01:23 +08:00
e0e31c765b feat: 增加抄送标记未读功能 2026-04-22 18:00:41 +08:00
335dc88a2a 综合运营:分支图BUG、跳转
feat:完善进度/总数
2026-04-22 16:32:43 +08:00
50527f68e0 添加我的审核,我的申请提前结束 2026-04-22 16:11:43 +08:00
8b78e82a80 添加1报工审批2添加我的申请 2026-04-22 15:52:58 +08:00
8b3e016568 去掉审批历史静态路由 2026-04-22 13:35:52 +08:00
c1c3fdba68 Merge remote-tracking branch 'origin/main' 2026-04-22 13:12:41 +08:00
2d86713971 出差新增检索 2026-04-22 13:10:54 +08:00
f3d5556196 feat:完善出差申请,城市页面优化 2026-04-21 16:32:24 +08:00
f831f29b63 feat: 完善城市管理功能 2026-04-21 10:47:01 +08:00
03b0e20301 feat: 完成城市管理前端增删改查功能 2026-04-20 20:02:35 +08:00
ba5796984c Merge branch 'main' of http://49.232.154.205:10100/liujingchao/fad_oa 2026-04-20 17:52:40 +08:00
a69c1f0cb2 feat:完成城市管理增刪改查功能 2026-04-20 17:51:59 +08:00
e728a98dcc 出差新增检索 2026-04-20 15:55:00 +08:00
54b820cc40 Merge remote-tracking branch 'origin/main' 2026-04-20 15:43:51 +08:00
f73a002f0f 新增城市管理 2026-04-20 14:14:08 +08:00
0400398361 feat(geolocation): 完善工作地点获取功能
- 新增高德地图逆地理编码接口,支持根据浏览器定位自动获取工作地点。
- 在项目报工页面中实现工作地点的自动更新,用户可通过按钮重新获取定位。
- 添加加载状态和错误提示,提升用户体验。
- 优化相关方法以处理地理位置获取的异步操作。
2026-04-17 13:57:27 +08:00
edca68136c feat(geolocation): 添加工作地点自动获取功能
- 在项目报工页面中新增工作地点自动获取功能,用户可通过点击按钮重新获取定位。
- 添加了工作地点加载状态和错误提示,提升用户体验。
- 优化了相关方法以处理地理位置获取的异步操作。
2026-04-17 13:06:39 +08:00
1584d7e06d 提交app同步后端 2026-04-17 12:05:15 +08:00
7c261c3028 feat: 完善高德地图地点搜索功能 2026-04-17 10:36:18 +08:00
5d0c056449 feat: 完成出差目的地高德地图选择功能(2) 2026-04-16 16:22:05 +08:00
fd72c18d48 feat: 完成出差目的地高德地图选择功能 2026-04-16 15:37:08 +08:00
dfd912bf07 feat(security): 新增高德逆地理编码接口并配置RestTemplate超时时间 2026-04-16 11:05:02 +08:00
8b627c000f Merge branch 'main' of http://49.232.154.205:10100/liujingchao/fad_oa 2026-04-15 18:39:01 +08:00
56f7a6abb9 feat:完成出差申请提前结束功能 2026-04-15 18:32:59 +08:00
307 changed files with 7993 additions and 5664 deletions

View File

@@ -6,9 +6,12 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
import com.ruoyi.hrm.service.IHrmAppropriationReqService;
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -28,9 +31,11 @@ import java.util.List;
public class HrmAppropriationReqController extends BaseController {
private final IHrmAppropriationReqService service;
private final IHrmInvoiceOcrService invoiceOcrService;
@GetMapping("/list")
public TableDataInfo<HrmAppropriationReqVo> list(HrmAppropriationReqBo bo, PageQuery pageQuery) {
bo.setCreateBy(LoginHelper.getUsername());
return service.queryPageList(bo, pageQuery);
}
@@ -59,7 +64,18 @@ public class HrmAppropriationReqController extends BaseController {
@GetMapping("/all")
public R<List<HrmAppropriationReqVo>> all(HrmAppropriationReqBo bo) {
bo.setCreateBy(String.valueOf(LoginHelper.getUserId()));
return R.ok(service.queryList(bo));
}
@GetMapping("/ocr-health")
public R<Boolean> ocrHealth() {
return R.ok(invoiceOcrService.isAlive());
}
@PostMapping("/ocr-by-oss")
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
}
}

View File

@@ -64,11 +64,9 @@ public class HrmEmployeeController extends BaseController {
*/
@GetMapping("/byUserId/{userId}")
public R<HrmEmployeeVo> getByUserId(@PathVariable @NotNull Long userId) {
HrmEmployeeBo bo = new HrmEmployeeBo();
bo.setUserId(userId);
List<HrmEmployeeVo> list = service.queryList(bo);
if (list != null && !list.isEmpty()) {
return R.ok(list.get(0));
HrmEmployeeVo vo = service.queryByUserId(userId);
if (vo != null) {
return R.ok(vo);
}
return R.fail("未找到该用户对应的员工信息");
}

View File

@@ -66,7 +66,14 @@ public class HrmFlowCcController extends BaseController {
Long userId = LoginHelper.getUserId();
return toAjax(service.markRead(ccId, userId));
}
/**
* 标记抄送未读(新增)
*/
@PostMapping("/{ccId}/unread")
public R<Void> unread(@PathVariable Long ccId) {
Long userId = LoginHelper.getUserId();
return toAjax(service.markUnread(ccId, userId));
}
@GetMapping("/ping")
public R<String> ping(@RequestParam @NotNull String x) {
return R.ok(x);

View File

@@ -56,6 +56,16 @@ public class HrmFlowTaskController extends BaseController {
}
/**
* 详情页使用:按 bizType + bizId 查询审批概要、当前待办和历史记录
*/
@GetMapping("/detailByBiz")
public R<?> detailByBiz(@RequestParam @NotNull String bizType,
@RequestParam @NotNull Long bizId,
@RequestParam(required = false) Long assigneeUserId) {
return R.ok(service.queryDetailByBiz(bizType, bizId, assigneeUserId));
}
@GetMapping("/{taskId}")
public R<HrmFlowTaskVo> getInfo(@PathVariable @NotNull Long taskId) {
return R.ok(service.queryById(taskId));

View File

@@ -6,7 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.hrm.domain.HrmLeaveReq;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.bo.HrmLeaveReqBo;
import com.ruoyi.hrm.domain.vo.HrmLeaveReqVo;
import com.ruoyi.hrm.domain.vo.HrmLeaveStatsVo;
@@ -30,6 +30,7 @@ public class HrmLeaveReqController extends BaseController {
@GetMapping("/list")
public TableDataInfo<HrmLeaveReqVo> list(HrmLeaveReqBo bo, PageQuery pageQuery) {
bo.setCreateBy(LoginHelper.getUsername());
return service.queryPageList(bo, pageQuery);
}
@@ -58,6 +59,7 @@ public class HrmLeaveReqController extends BaseController {
@GetMapping("/all")
public R<List<HrmLeaveReqVo>> all(HrmLeaveReqBo bo) {
bo.setCreateBy(LoginHelper.getUsername());
return R.ok(service.queryList(bo));
}

View File

@@ -0,0 +1,127 @@
package com.ruoyi.hrm.controller;
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.page.TableDataInfo;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.HrmEmployee;
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
import com.ruoyi.hrm.domain.bo.HrmLeaveReqBo;
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
import com.ruoyi.hrm.domain.bo.HrmSealReqBo;
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
import com.ruoyi.hrm.domain.vo.HrmLeaveReqVo;
import com.ruoyi.hrm.domain.vo.HrmMyApplyVo;
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
import com.ruoyi.hrm.domain.vo.HrmSealReqVo;
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
import com.ruoyi.hrm.mapper.HrmAppropriationReqMapper;
import com.ruoyi.hrm.mapper.HrmEmployeeMapper;
import com.ruoyi.hrm.mapper.HrmLeaveReqMapper;
import com.ruoyi.hrm.mapper.HrmReimburseReqMapper;
import com.ruoyi.hrm.mapper.HrmSealReqMapper;
import com.ruoyi.hrm.mapper.HrmTravelReqMapper;
import com.ruoyi.system.mapper.SysUserMapper;
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.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@RestController
@RequestMapping("/hrm/my-apply")
public class HrmMyApplyController extends BaseController {
private final HrmEmployeeMapper employeeMapper;
private final SysUserMapper sysUserMapper;
private final HrmLeaveReqMapper leaveReqMapper;
private final HrmTravelReqMapper travelReqMapper;
private final HrmSealReqMapper sealReqMapper;
private final HrmReimburseReqMapper reimburseReqMapper;
private final HrmAppropriationReqMapper appropriationReqMapper;
@GetMapping("/list")
public TableDataInfo<HrmMyApplyVo> list(String bizType, String status, String keyword, PageQuery pageQuery) {
Long currentUserId = LoginHelper.getUserId();
if (currentUserId == null) {
return TableDataInfo.build();
}
HrmEmployee emp = employeeMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<HrmEmployee>()
.eq(HrmEmployee::getUserId, currentUserId));
if (emp == null) {
return TableDataInfo.build();
}
SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId());
String nickName = sysUser != null ? sysUser.getNickName() : null;
List<HrmMyApplyVo> all = new ArrayList<>();
if (bizType == null || bizType.isEmpty() || "leave".equals(bizType)) {
all.addAll(mapLeave(leaveReqMapper.selectVoWithProjectList(buildLeaveBo(emp.getEmpId(), status)), nickName));
}
if (bizType == null || bizType.isEmpty() || "travel".equals(bizType)) {
all.addAll(mapTravel(travelReqMapper.selectVoWithProjectList(buildTravelBo(emp.getEmpId(), status)), nickName));
}
if (bizType == null || bizType.isEmpty() || "seal".equals(bizType)) {
all.addAll(mapSeal(sealReqMapper.selectVoWithProjectList(buildSealBo(emp.getEmpId(), status)), nickName));
}
if (bizType == null || bizType.isEmpty() || "reimburse".equals(bizType)) {
all.addAll(mapReimburse(reimburseReqMapper.selectVoWithProjectList(buildReimburseBo(emp.getEmpId(), status)), nickName));
}
if (bizType == null || bizType.isEmpty() || "appropriation".equals(bizType)) {
all.addAll(mapAppropriation(appropriationReqMapper.selectVoWithProjectList(buildAppropriationBo(emp.getEmpId(), status)), nickName));
}
if (keyword != null && !keyword.isEmpty()) {
String lower = keyword.toLowerCase();
all = all.stream().filter(v -> contains(v, lower)).collect(Collectors.toList());
}
all.sort(Comparator.comparing(HrmMyApplyVo::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())).reversed());
long start = (pageQuery.getPageNum() - 1L) * pageQuery.getPageSize();
long end = Math.min(start + pageQuery.getPageSize(), all.size());
Page<HrmMyApplyVo> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize(), all.size());
page.setRecords(start >= all.size() ? new ArrayList<>() : all.subList((int) start, (int) end));
return TableDataInfo.build(page);
}
private boolean contains(HrmMyApplyVo v, String lower) {
return Objects.toString(v.getTitle(), "").toLowerCase().contains(lower)
|| Objects.toString(v.getRemark(), "").toLowerCase().contains(lower)
|| Objects.toString(v.getNickName(), "").toLowerCase().contains(lower)
|| Objects.toString(v.getEmpName(), "").toLowerCase().contains(lower)
|| Objects.toString(v.getBizId(), "").contains(lower);
}
private HrmLeaveReqBo buildLeaveBo(Long empId, String status) { HrmLeaveReqBo bo = new HrmLeaveReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
private HrmTravelReqBo buildTravelBo(Long empId, String status) { HrmTravelReqBo bo = new HrmTravelReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
private HrmSealReqBo buildSealBo(Long empId, String status) { HrmSealReqBo bo = new HrmSealReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
private HrmReimburseReqBo buildReimburseBo(Long empId, String status) { HrmReimburseReqBo bo = new HrmReimburseReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
private HrmAppropriationReqBo buildAppropriationBo(Long empId, String status) { HrmAppropriationReqBo bo = new HrmAppropriationReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
private List<HrmMyApplyVo> mapLeave(List<HrmLeaveReqVo> list, String nickName) { return list.stream().map(v -> toVo("leave", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
private List<HrmMyApplyVo> mapTravel(List<HrmTravelReqVo> list, String nickName) { return list.stream().map(v -> toVo("travel", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
private List<HrmMyApplyVo> mapSeal(List<HrmSealReqVo> list, String nickName) { return list.stream().map(v -> toVo("seal", v.getBizId(), v.getEmpId(), nickName, v.getRemark(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
private List<HrmMyApplyVo> mapReimburse(List<HrmReimburseReqVo> list, String nickName) { return list.stream().map(v -> toVo("reimburse", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
private List<HrmMyApplyVo> mapAppropriation(List<HrmAppropriationReqVo> list, String nickName) { return list.stream().map(v -> toVo("appropriation", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
private HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime) {
HrmMyApplyVo vo = new HrmMyApplyVo();
vo.setBizType(bizType);
vo.setBizId(bizId);
vo.setEmpId(empId);
vo.setNickName(nickName);
vo.setTitle(title);
vo.setStatus(status);
vo.setCreateTime(createTime);
return vo;
}
}

View File

@@ -6,8 +6,11 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
import com.ruoyi.hrm.service.IHrmReimburseReqService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
@@ -28,9 +31,11 @@ import java.util.List;
public class HrmReimburseReqController extends BaseController {
private final IHrmReimburseReqService service;
private final IHrmInvoiceOcrService invoiceOcrService;
@GetMapping("/list")
public TableDataInfo<HrmReimburseReqVo> list(HrmReimburseReqBo bo, PageQuery pageQuery) {
bo.setCreateBy(LoginHelper.getUsername());
return service.queryPageList(bo, pageQuery);
}
@@ -59,7 +64,18 @@ public class HrmReimburseReqController extends BaseController {
@GetMapping("/all")
public R<List<HrmReimburseReqVo>> all(HrmReimburseReqBo bo) {
bo.setCreateBy(LoginHelper.getUsername());
return R.ok(service.queryList(bo));
}
@GetMapping("/ocr-health")
public R<Boolean> ocrHealth() {
return R.ok(invoiceOcrService.isAlive());
}
@PostMapping("/ocr-by-oss")
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
}
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.bo.HrmSealReqBo;
import com.ruoyi.hrm.domain.bo.HrmSealStampBo;
import com.ruoyi.hrm.domain.vo.HrmSealReqVo;
@@ -32,6 +33,7 @@ public class HrmSealReqController extends BaseController {
@GetMapping("/list")
public TableDataInfo<HrmSealReqVo> list(HrmSealReqBo bo, PageQuery pageQuery) {
bo.setCreateBy(LoginHelper.getUsername());
return service.queryPageList(bo, pageQuery);
}
@@ -40,6 +42,11 @@ public class HrmSealReqController extends BaseController {
return R.ok(service.queryById(bizId));
}
@GetMapping("/{bizId}/pdfPages")
public R<Integer> pdfPages(@PathVariable @NotNull Long bizId) {
return R.ok(service.queryPdfPageTotal(bizId));
}
@Log(title = "用印申请", businessType = BusinessType.INSERT)
@PostMapping
public R<HrmSealReqVo> add(@Validated @RequestBody HrmSealReqBo bo) {

View File

@@ -6,10 +6,12 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
import com.ruoyi.hrm.service.IHrmTravelReqService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -28,9 +30,17 @@ import java.util.List;
public class HrmTravelReqController extends BaseController {
private final IHrmTravelReqService service;
private final IHrmTravelReqService hrmTravelReqService;
@Value("${fad.amap.webkey}")
private String amapKey;
@Value("${fad.amap.securitykey}")
private String amapSecurityKey;
@GetMapping("/list")
public TableDataInfo<HrmTravelReqVo> list(HrmTravelReqBo bo, PageQuery pageQuery) {
bo.setCreateBy(LoginHelper.getUsername());
return service.queryPageList(bo, pageQuery);
}
@@ -56,9 +66,24 @@ public class HrmTravelReqController extends BaseController {
public R<Void> remove(@PathVariable @NotEmpty Long[] bizIds) {
return toAjax(service.deleteWithValidByIds(Arrays.asList(bizIds), true));
}
@PutMapping("/earlyEnd/{bizId}")
public R<Void> earlyEnd(@PathVariable Long bizId) {
return toAjax(hrmTravelReqService.earlyEnd(bizId));
}
@GetMapping("/amapKey")
public R<String> getAmapKey() {
return R.ok(amapKey);
}
@GetMapping("/amapSecurityKey")
public R<String> getAmapSecurityKey() {
return R.ok(amapSecurityKey);
}
@GetMapping("/all")
public R<List<HrmTravelReqVo>> all(HrmTravelReqBo bo) {
bo.setCreateBy(LoginHelper.getUsername());
return R.ok(service.queryList(bo));
}
}

View File

@@ -7,12 +7,11 @@ import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hrm_flow_action")
public class HrmFlowAction extends BaseEntity implements Serializable {
public class HrmFlowAction extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId

View File

@@ -0,0 +1,43 @@
package com.ruoyi.hrm.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
/**
* 发票条目子表(报销单/拨款单共用)
*/
@Data
@TableName("hrm_invoice_item")
public class HrmInvoiceItem {
@TableId
private Long id;
/** 业务类型 reimburse / appropriation */
private String bizType;
/** 关联业务单ID */
private Long bizId;
/** 来源附件ossId */
private Long ossId;
/** 排序序号 */
private Integer sortNo;
/** OCR识别项目名称 */
private String itemName;
/** 事由说明(用户可编辑) */
private String reason;
/** 金额 */
private BigDecimal amount;
@TableLogic
private Integer delFlag;
}

View File

@@ -7,7 +7,6 @@ import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 用印申请
@@ -15,7 +14,7 @@ import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hrm_seal_req")
public class HrmSealReq extends BaseEntity implements Serializable {
public class HrmSealReq extends BaseEntity {
private static final long serialVersionUID = 1L;
@@ -29,7 +28,7 @@ public class HrmSealReq extends BaseEntity implements Serializable {
/** 项目ID */
private Long projectId;
/** 用印类型(公章/合同章/财务章等) */
/** 用印类型(对应印章文件名) */
private String sealType;
/** 用途说明 */

View File

@@ -32,6 +32,7 @@ public class HrmTravelReq extends BaseEntity {
private String bankName;
private String bankAccount;
private String remark;
private Date actualEndTime;
@TableLogic
private Integer delFlag;
}

View File

@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
/**
* 拨款申请 Bo
@@ -59,5 +60,8 @@ public class HrmAppropriationReqBo extends BaseEntity {
private String remark;
private Long tplId;
/** 发票条目列表(前端提交时携带) */
private List<HrmInvoiceItemBo> invoiceItems;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.hrm.domain.bo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 发票条目 BO用于表单提交
*/
@Data
public class HrmInvoiceItemBo {
/** 来源附件ossId */
private Long ossId;
/** 排序序号 */
private Integer sortNo;
/** OCR识别项目名称 */
private String itemName;
/** 事由说明 */
private String reason;
/** 金额 */
private BigDecimal amount;
}

View File

@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@@ -39,5 +40,8 @@ public class HrmReimburseReqBo extends BaseEntity {
private String remark;
private Long tplId;
/** 发票条目列表(前端提交时携带) */
private List<HrmInvoiceItemBo> invoiceItems;
}

View File

@@ -24,7 +24,7 @@ public class HrmSealReqBo extends BaseEntity {
/** 项目ID */
private Long projectId;
/** 用印类型 */
/** 用印类型(对应印章文件名) */
@NotBlank(message = "用印类型不能为空")
private String sealType;

View File

@@ -6,6 +6,8 @@ import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.ruoyi.hrm.domain.HrmInvoiceItem;
/**
* 拨款申请 VO
@@ -108,5 +110,7 @@ public class HrmAppropriationReqVo implements Serializable {
/** 流程实例ID */
private Long instId;
private List<HrmInvoiceItem> invoiceItems;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.hrm.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class HrmFlowActionTimelineVo implements Serializable {
private static final long serialVersionUID = 1L;
private Long actionId;
private Long taskId;
private Long instId;
private Long actionUserId;
private String actionUserName;
private Long assigneeUserId;
private String assigneeUserName;
private String action;
private String actionText;
private String remark;
private String bizType;
private Long bizId;
private Long nodeId;
private String nodeName;
private String taskStatus;
private Date createTime;
}

View File

@@ -34,6 +34,8 @@ public class HrmFlowInstanceVo implements Serializable {
private Date endTime;
private BigDecimal hours;
private String procStatus;
private Date actualEndTime;
private String statusName;
private String createBy;
private Date createTime;

View File

@@ -0,0 +1,32 @@
package com.ruoyi.hrm.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class HrmFlowTaskDetailVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 当前业务对应的待办任务 */
private HrmFlowTaskVo currentTask;
/** 当前业务对应的全部任务历史 */
private List<HrmFlowTaskVo> taskHistory;
/** 当前流程实例状态 */
private String flowStatus;
/** 当前节点ID */
private Long currentNodeId;
/** 当前节点名称 */
private String currentNodeName;
/** 审批是否通过 */
private Boolean approved;
/** 流程动作历史(更细粒度) */
private List<HrmFlowActionTimelineVo> actionTimeline;
}

View File

@@ -0,0 +1,38 @@
package com.ruoyi.hrm.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 单张发票OCR识别结果返回给前端用于填充发票条目
*/
@Data
public class HrmInvoiceOcrResultVo {
/** 发票类型 */
private String invoiceType;
/** 销售方名称 */
private String sellerName;
/** 开票日期 */
private String invoiceDate;
/** 价税合计 */
private BigDecimal totalAmount;
/** 识别出的条目列表 */
private List<Item> items;
@Data
public static class Item {
/** OCR识别名称 */
private String itemName;
/** 金额(不含税) */
private BigDecimal amount;
/** 税率 */
private String taxRate;
}
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.hrm.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class HrmMyApplyVo implements Serializable {
private static final long serialVersionUID = 1L;
private String bizType;
private Long bizId;
private Long empId;
private String empName;
private Long userId;
private String nickName;
private String title;
private String status;
private Date createTime;
private Date endTime;
private Date actualEndTime;
private String remark;
}

View File

@@ -6,6 +6,8 @@ import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.ruoyi.hrm.domain.HrmInvoiceItem;
@Data
public class HrmReimburseReqVo implements Serializable {
@@ -95,5 +97,7 @@ public class HrmReimburseReqVo implements Serializable {
private Date updateTime;
private Long instId;
private List<HrmInvoiceItem> invoiceItems;
}

View File

@@ -88,6 +88,9 @@ public class HrmSealReqVo implements Serializable {
@Excel(name = "回执附件ID列表")
private String receiptFileIds;
@Excel(name = "PDF页数")
private Integer pdfPageTotal;
@Excel(name = "状态")
private String status;

View File

@@ -74,6 +74,8 @@ public class HrmTravelReqVo implements Serializable {
private String bankAccount;
@Excel(name = "备注")
private String remark;
private Date actualEndTime;
private String createBy;
private Date createTime;
private String updateBy;

View File

@@ -0,0 +1,7 @@
package com.ruoyi.hrm.mapper;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.hrm.domain.HrmInvoiceItem;
public interface HrmInvoiceItemMapper extends BaseMapperPlus<HrmInvoiceItemMapper, HrmInvoiceItem, HrmInvoiceItem> {
}

View File

@@ -15,6 +15,8 @@ public interface IHrmEmployeeService {
List<HrmEmployeeVo> queryList(HrmEmployeeBo bo);
HrmEmployeeVo queryByUserId(Long userId);
Boolean insertByBo(HrmEmployeeBo bo);
Boolean updateByBo(HrmEmployeeBo bo);

View File

@@ -27,5 +27,9 @@ public interface IHrmFlowCcService {
* 标记已读
*/
Boolean markRead(Long ccId, Long userId);
/**
* 标记未读
*/
Boolean markUnread(Long ccId, Long userId);
}

View File

@@ -2,8 +2,8 @@ package com.ruoyi.hrm.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.hrm.domain.HrmFlowTask;
import com.ruoyi.hrm.domain.bo.HrmFlowTaskBo;
import com.ruoyi.hrm.domain.vo.HrmFlowTaskDetailVo;
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
import java.util.Collection;
@@ -51,4 +51,9 @@ public interface IHrmFlowTaskService {
* 根据业务类型 + 业务ID 查询当前待办任务pending用于详情页自动带出 currentTaskId
*/
HrmFlowTaskVo queryTodoByBiz(String bizType, Long bizId, Long assigneeUserId);
/**
* 按业务查询详情:当前待办、状态和历史审批记录
*/
HrmFlowTaskDetailVo queryDetailByBiz(String bizType, Long bizId, Long assigneeUserId);
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.hrm.service;
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
/**
* 发票OCR识别服务调用Python OCR微服务
*/
public interface IHrmInvoiceOcrService {
/**
* 通过ossId识别发票
*/
HrmInvoiceOcrResultVo recognizeByOssId(Long ossId);
/**
* 检查OCR服务是否存活
*/
boolean isAlive();
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.hrm.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.hrm.domain.vo.HrmMyApplyVo;
public interface IHrmMyApplyService {
TableDataInfo<HrmMyApplyVo> queryPageList(String bizType, String status, String keyword, PageQuery pageQuery);
}

View File

@@ -13,6 +13,8 @@ public interface IHrmSealReqService {
HrmSealReqVo queryById(Long bizId);
Integer queryPdfPageTotal(Long bizId);
TableDataInfo<HrmSealReqVo> queryPageList(HrmSealReqBo bo, PageQuery pageQuery);
List<HrmSealReqVo> queryList(HrmSealReqBo bo);

View File

@@ -22,5 +22,7 @@ public interface IHrmTravelReqService {
Boolean updateByBo(HrmTravelReqBo bo);
int earlyEnd(Long bizId);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -9,11 +9,14 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.HrmAppropriationReq;
import com.ruoyi.hrm.domain.HrmFlowTemplate;
import com.ruoyi.hrm.domain.HrmInvoiceItem;
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
import com.ruoyi.hrm.domain.bo.HrmInvoiceItemBo;
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
import com.ruoyi.hrm.mapper.HrmAppropriationReqMapper;
import com.ruoyi.hrm.mapper.HrmFlowTemplateMapper;
import com.ruoyi.hrm.mapper.HrmInvoiceItemMapper;
import com.ruoyi.hrm.service.IHrmAppropriationReqService;
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
import lombok.RequiredArgsConstructor;
@@ -33,10 +36,18 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
private final HrmAppropriationReqMapper baseMapper;
private final HrmFlowTemplateMapper flowTemplateMapper;
private final IHrmFlowInstanceService flowInstanceService;
private final HrmInvoiceItemMapper invoiceItemMapper;
@Override
public HrmAppropriationReqVo queryById(Long bizId) {
return baseMapper.selectVoWithProjectById(bizId);
HrmAppropriationReqVo vo = baseMapper.selectVoWithProjectById(bizId);
if (vo != null) {
vo.setInvoiceItems(invoiceItemMapper.selectList(Wrappers.<HrmInvoiceItem>lambdaQuery()
.eq(HrmInvoiceItem::getBizType, "appropriation")
.eq(HrmInvoiceItem::getBizId, bizId)
.orderByAsc(HrmInvoiceItem::getSortNo)));
}
return vo;
}
@Override
@@ -64,6 +75,12 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
boolean ok = baseMapper.insert(add) > 0;
HrmAppropriationReqVo bean = BeanUtil.toBean(add, HrmAppropriationReqVo.class);
// 保存发票条目
if (ok && bo.getInvoiceItems() != null && !bo.getInvoiceItems().isEmpty()) {
saveInvoiceItems("appropriation", add.getBizId(), bo.getInvoiceItems());
}
if (ok && "pending".equalsIgnoreCase(add.getStatus())) {
// 获取流程启动人ID
Long startUserId = LoginHelper.getUserId();
@@ -108,7 +125,11 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(HrmAppropriationReqBo bo) {
HrmAppropriationReq update = BeanUtil.toBean(bo, HrmAppropriationReq.class);
return baseMapper.updateById(update) > 0;
boolean updated = baseMapper.updateById(update) > 0;
if (updated && bo.getInvoiceItems() != null) {
saveInvoiceItems("appropriation", bo.getBizId(), bo.getInvoiceItems());
}
return updated;
}
@Override
@@ -125,6 +146,24 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
return baseMapper.updateById(req) > 0;
}
private void saveInvoiceItems(String bizType, Long bizId, List<HrmInvoiceItemBo> boList) {
invoiceItemMapper.delete(Wrappers.<HrmInvoiceItem>lambdaQuery()
.eq(HrmInvoiceItem::getBizType, bizType)
.eq(HrmInvoiceItem::getBizId, bizId));
for (int i = 0; i < boList.size(); i++) {
HrmInvoiceItemBo bo = boList.get(i);
HrmInvoiceItem item = new HrmInvoiceItem();
item.setBizType(bizType);
item.setBizId(bizId);
item.setOssId(bo.getOssId());
item.setSortNo(bo.getSortNo() != null ? bo.getSortNo() : i);
item.setItemName(bo.getItemName());
item.setReason(bo.getReason());
item.setAmount(bo.getAmount());
invoiceItemMapper.insert(item);
}
}
@SuppressWarnings("unused")
private LambdaQueryWrapper<HrmAppropriationReq> buildQueryWrapper(HrmAppropriationReqBo bo) {
LambdaQueryWrapper<HrmAppropriationReq> lqw = Wrappers.lambdaQuery();

View File

@@ -29,6 +29,11 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService {
return baseMapper.selectVoById(empId);
}
@Override
public HrmEmployeeVo queryByUserId(Long userId) {
return baseMapper.selectVoOne(Wrappers.<HrmEmployee>lambdaQuery().eq(HrmEmployee::getUserId, userId));
}
@Override
public TableDataInfo<HrmEmployeeVo> queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<HrmEmployee> lqw = buildQueryWrapper(bo);

View File

@@ -99,6 +99,23 @@ public class HrmFlowCcServiceImpl implements IHrmFlowCcService {
.eq(HrmFlowCc::getDelFlag, 0)
) > 0;
}
/**
* 标记未读
*/
@Override
public Boolean markUnread(Long ccId, Long userId) {
if (ccId == null || userId == null) {
return false;
}
return baseMapper.update(
null,
Wrappers.<HrmFlowCc>lambdaUpdate()
.set(HrmFlowCc::getReadFlag, 0)
.eq(HrmFlowCc::getCcId, ccId)
.eq(HrmFlowCc::getCcUserId, userId)
.eq(HrmFlowCc::getDelFlag, 0)
) > 0;
}
private LambdaQueryWrapper<HrmFlowCc> buildQueryWrapper(HrmFlowCcBo bo) {
LambdaQueryWrapper<HrmFlowCc> lqw = Wrappers.lambdaQuery();

View File

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.service.UserService;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.HrmFlowInstance;
import com.ruoyi.hrm.domain.HrmFlowNode;
import com.ruoyi.hrm.domain.HrmFlowTask;
@@ -15,6 +16,7 @@ import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
import com.ruoyi.hrm.domain.bo.HrmFlowInstanceBo;
import com.ruoyi.hrm.domain.vo.HrmFlowInstanceVo;
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
import com.ruoyi.hrm.mapper.*;
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
import lombok.RequiredArgsConstructor;
@@ -37,6 +39,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
private final FlowAssigneeHelper assigneeHelper;
private final UserService userService;
private final HrmFlowCcMapper ccMapper;
private final HrmTravelReqMapper travelReqMapper;
@Override
public HrmFlowInstanceVo queryById(Long instId) {
@@ -75,6 +78,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
task.setNodeId(0L);
task.setAssigneeUserId(bo.getManualAssigneeUserId());
task.setStatus("pending");
task.setRemark("自选审批人一次性审批");
// 关键:写入业务关联字段,便于审批中心联查业务数据
task.setBizType(bo.getBizType());
task.setBizId(bo.getBizId());
@@ -121,6 +125,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
task.setNodeId(firstNode.getNodeId());
task.setAssigneeUserId(assignees.get(0));
task.setStatus("pending");
task.setRemark(firstNode.getRemark());
// 关键:写入业务关联字段,便于审批中心联查业务数据
task.setBizType(bo.getBizType());
task.setBizId(bo.getBizId());
@@ -140,12 +145,36 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
public TableDataInfo<HrmFlowInstanceVo> queryMyInstancePageList(HrmFlowInstanceBo bo, PageQuery pageQuery) {
// “我的申请”= 当前登录用户发起的流程实例
// 这里不信任前端传 startUserId统一以登录态为准
Long userId = com.ruoyi.common.helper.LoginHelper.getUserId();
Long userId = LoginHelper.getUserId();
LambdaQueryWrapper<HrmFlowInstance> lqw = buildQueryWrapper(bo);
lqw.eq(userId != null, HrmFlowInstance::getStartUserId, userId);
// 默认按发起时间倒序如果表里没createTime字段这里可改成instId倒序
lqw.orderByDesc(HrmFlowInstance::getInstId);
Page<HrmFlowInstanceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
for (HrmFlowInstanceVo record : result.getRecords()) {
// 写入出差相关的时间,用于在页面中可以提前结束
if (record.getBizType().equals("travel")){
HrmTravelReqVo hrmTravelReqVo = travelReqMapper.selectVoById(record.getBizId());
record.setActualEndTime(hrmTravelReqVo.getActualEndTime());
}
}
for (HrmFlowInstanceVo vo : result.getRecords()) {
if ("travel".equals(vo.getBizType())) {
HrmTravelReqVo travel = travelReqMapper.selectVoById(vo.getBizId());
if (travel != null) {
vo.setActualEndTime(travel.getActualEndTime());
// 只有流程已完成,才替换显示文字
if ("complete".equals(vo.getStatus())) {
if (travel.getActualEndTime() == null) {
vo.setStatusName("出差中");
} else {
vo.setStatusName("已结束");
}
}
}
}
}
return TableDataInfo.build(result);
}

View File

@@ -12,11 +12,15 @@ import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.hrm.domain.*;
import com.ruoyi.hrm.domain.bo.HrmFlowTaskBo;
import com.ruoyi.hrm.domain.bo.HrmSealStampBo;
import com.ruoyi.hrm.domain.vo.HrmFlowActionTimelineVo;
import com.ruoyi.hrm.domain.vo.HrmFlowTaskDetailVo;
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
import com.ruoyi.hrm.domain.vo.HrmEmployeeVo;
import com.ruoyi.hrm.mapper.*;
import com.ruoyi.hrm.service.IHrmFlowTaskService;
import com.ruoyi.hrm.service.IHrmSealReqService;
import com.ruoyi.hrm.service.IHrmFlowCcService;
import com.ruoyi.hrm.service.IHrmEmployeeService;
import com.ruoyi.hrm.domain.bo.HrmFlowCcBo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -38,7 +42,6 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
private final FlowAssigneeHelper assigneeHelper;
private final BizStatusSyncHelper bizStatusSyncHelper;
private final HrmFlowTaskMapper hrmFlowTaskMapper;
// 注入五个业务Mapper
private final HrmLeaveReqMapper leaveReqMapper;
private final HrmTravelReqMapper travelReqMapper;
@@ -47,6 +50,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
private final HrmAppropriationReqMapper appropriationReqMapper;
private final ObjectMapper objectMapper; // Spring Boot 默认提供
private final UserService userService;
private final IHrmEmployeeService employeeService;
@Override
public HrmFlowTaskVo queryById(Long taskId) {
@@ -74,7 +78,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
return tasks;
}
private void fillBizData(List<HrmFlowTaskVo> tasks) {
private void fillBizData(List<HrmFlowTaskVo> tasks) {
// 1. 按 bizType 分组,并收集 bizId
Map<String, List<Long>> bizIdsByType = tasks.stream()
.filter(t -> t.getBizType() != null && t.getBizId() != null)
@@ -90,19 +94,39 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
if (bizIds.isEmpty()) return;
switch (bizType) {
case "leave":
leaveReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("leave_" + d.getBizId(), d));
leaveReqMapper.selectBatchIds(bizIds).forEach(d -> {
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
fillEmpName(dataMap, d.getEmpId());
bizDataMap.put("leave_" + d.getBizId(), dataMap);
});
break;
case "travel":
travelReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("travel_" + d.getBizId(), d));
travelReqMapper.selectBatchIds(bizIds).forEach(d -> {
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
fillEmpName(dataMap, d.getEmpId());
bizDataMap.put("travel_" + d.getBizId(), dataMap);
});
break;
case "seal":
sealReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("seal_" + d.getBizId(), d));
sealReqMapper.selectBatchIds(bizIds).forEach(d -> {
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
fillEmpName(dataMap, d.getEmpId());
bizDataMap.put("seal_" + d.getBizId(), dataMap);
});
break;
case "reimburse":
reimburseReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("reimburse_" + d.getBizId(), d));
reimburseReqMapper.selectBatchIds(bizIds).forEach(d -> {
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
fillEmpName(dataMap, d.getEmpId());
bizDataMap.put("reimburse_" + d.getBizId(), dataMap);
});
break;
case "appropriation":
appropriationReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("appropriation_" + d.getBizId(), d));
appropriationReqMapper.selectBatchIds(bizIds).forEach(d -> {
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
fillEmpName(dataMap, d.getEmpId());
bizDataMap.put("appropriation_" + d.getBizId(), dataMap);
});
break;
}
});
@@ -112,13 +136,20 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
String key = task.getBizType() + "_" + task.getBizId();
Object data = bizDataMap.get(key);
if (data != null) {
// 将实体对象转换为 Map<String, Object>,方便前端使用
Map<String, Object> dataMap = objectMapper.convertValue(data, Map.class);
task.setBizData(dataMap);
task.setBizData((Map<String, Object>) data);
}
});
}
private void fillEmpName(Map<String, Object> dataMap, Long empId) {
if (empId != null) {
HrmEmployeeVo emp = employeeService.queryById(empId);
if (emp != null && emp.getEmpName() != null) {
dataMap.put("empName", emp.getEmpName());
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@@ -155,12 +186,13 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
if (inst == null) {
return false;
}
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
// 无模板一次性审批tplId=0 或 nodeId=0直接结束流程
if (inst.getTplId() != null && inst.getTplId() == 0L) {
// 记录动作
saveAction(taskId, inst.getInstId(), "approve", remark, actionUserId,task.getBizType(), task.getBizId());
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "approve", remark, operatorUserId);
if (stampBo != null) {
saveAction(taskId, inst.getInstId(), "stamp", "盖章", actionUserId,task.getBizType(), task.getBizId());
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "stamp", "盖章", operatorUserId);
}
task.setStatus("approved");
baseMapper.updateById(task);
@@ -172,7 +204,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
sealReqService.updateStatus(inst.getBizId(), "approved");
if (stampBo != null) {
// 盖章动作也写入流转历史
saveAction(taskId, inst.getInstId(), "stamp", "盖章", actionUserId,task.getBizType(), task.getBizId());
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "stamp", "盖章", operatorUserId);
sealReqService.stampWithJava(inst.getBizId(), stampBo);
}
}
@@ -185,7 +217,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
return false;
}
// 记录动作
saveAction(taskId, inst.getInstId(), "approve", remark, actionUserId,task.getBizType(),task.getBizId());
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "approve", remark, operatorUserId);
// 完成当前任务
task.setStatus("approved");
baseMapper.updateById(task);
@@ -269,7 +301,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
if (inst == null) {
return false;
}
saveAction(taskId, inst.getInstId(), "reject", remark, actionUserId,task.getBizType(),task.getBizId());
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "reject", remark, operatorUserId);
task.setStatus("rejected");
baseMapper.updateById(task);
inst.setStatus("rejected");
@@ -293,7 +326,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
if (inst == null) {
return false;
}
saveAction(taskId, inst.getInstId(), "withdraw", remark, actionUserId, task.getBizType(), task.getBizId());
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "withdraw", remark, operatorUserId);
task.setStatus("withdraw");
baseMapper.updateById(task);
// 无模板一次性审批:撤回后业务回到 pending并重新生成一个待办仍然只允许一次审批
@@ -330,13 +364,13 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
return true;
}
private void saveAction(Long taskId, Long instId, String action, String remark, Long userId, String bizType, Long bizId) {
private void saveAction(Long taskId, Long instId, String bizType, Long bizId, Long assigneeUserId, String action, String remark, Long actionUserId) {
HrmFlowAction log = new HrmFlowAction();
log.setTaskId(taskId);
log.setInstId(instId);
log.setAction(action);
log.setRemark(remark);
log.setActionUserId(userId);
log.setActionUserId(actionUserId);
log.setCreateTime(new Date());
log.setBizType(bizType);
log.setBizId(bizId);
@@ -358,7 +392,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
return false;
}
// 记录动作
saveAction(taskId, inst.getInstId(), "transfer", remark, actionUserId, task.getBizType(), task.getBizId());
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), newAssigneeUserId, "transfer", remark, operatorUserId);
// 更新办理人
HrmFlowTask u = new HrmFlowTask();
@@ -369,7 +404,6 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
@Override
public HrmFlowTaskVo queryTodoByBiz(String bizType, Long bizId, Long assigneeUserId) {
// 只取"待办 pending"的一条(理论上同一 biz 同一时刻最多一条待办)
LambdaQueryWrapper<HrmFlowTask> lqw = Wrappers.<HrmFlowTask>lambdaQuery()
.eq(bizType != null, HrmFlowTask::getBizType, bizType)
.eq(bizId != null, HrmFlowTask::getBizId, bizId)
@@ -378,12 +412,118 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
.orderByDesc(HrmFlowTask::getTaskId)
.last("limit 1");
HrmFlowTaskVo hrmFlowTaskVo = baseMapper.selectVoOne(lqw);
if (hrmFlowTaskVo != null) {
if (hrmFlowTaskVo != null && hrmFlowTaskVo.getAssigneeUserId() != null) {
hrmFlowTaskVo.setAssigneeNickName(userService.selectNickNameById(hrmFlowTaskVo.getAssigneeUserId()));
}
return hrmFlowTaskVo;
}
@Override
public HrmFlowTaskDetailVo queryDetailByBiz(String bizType, Long bizId, Long assigneeUserId) {
HrmFlowTaskDetailVo result = new HrmFlowTaskDetailVo();
HrmFlowTaskVo currentTask = queryTodoByBiz(bizType, bizId, assigneeUserId);
result.setCurrentTask(currentTask);
LambdaQueryWrapper<HrmFlowTask> historyQ = Wrappers.<HrmFlowTask>lambdaQuery()
.eq(bizType != null, HrmFlowTask::getBizType, bizType)
.eq(bizId != null, HrmFlowTask::getBizId, bizId)
.orderByAsc(HrmFlowTask::getCreateTime);
List<HrmFlowTaskVo> histories = baseMapper.selectVoList(historyQ);
if (histories != null) {
histories.forEach(task -> {
if (task.getAssigneeUserId() != null) {
task.setAssigneeNickName(userService.selectNickNameById(task.getAssigneeUserId()));
}
});
}
result.setTaskHistory(histories == null ? Collections.emptyList() : histories);
HrmFlowInstance inst = instanceMapper.selectOne(Wrappers.<HrmFlowInstance>lambdaQuery()
.eq(bizType != null, HrmFlowInstance::getBizType, bizType)
.eq(bizId != null, HrmFlowInstance::getBizId, bizId)
.orderByDesc(HrmFlowInstance::getInstId)
.last("limit 1"));
if (inst != null) {
result.setFlowStatus(inst.getStatus());
result.setCurrentNodeId(inst.getCurrentNodeId());
HrmFlowNode node = inst.getCurrentNodeId() == null ? null : nodeMapper.selectById(inst.getCurrentNodeId());
if (node != null) {
result.setCurrentNodeName(node.getRemark());
}
result.setApproved(Boolean.valueOf("approved".equalsIgnoreCase(inst.getStatus())));
}
result.setActionTimeline(buildActionTimeline(bizType, bizId));
return result;
}
private List<HrmFlowActionTimelineVo> buildActionTimeline(String bizType, Long bizId) {
LambdaQueryWrapper<HrmFlowAction> actionQ = Wrappers.<HrmFlowAction>lambdaQuery()
.eq(bizType != null, HrmFlowAction::getBizType, bizType)
.eq(bizId != null, HrmFlowAction::getBizId, bizId)
.orderByAsc(HrmFlowAction::getCreateTime);
List<HrmFlowAction> actions = actionMapper.selectList(actionQ);
if (actions == null || actions.isEmpty()) {
return Collections.emptyList();
}
Map<Long, HrmFlowTask> taskMap = new HashMap<>();
List<Long> taskIds = actions.stream().map(HrmFlowAction::getTaskId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (!taskIds.isEmpty()) {
baseMapper.selectBatchIds(taskIds).forEach(task -> taskMap.put(task.getTaskId(), task));
}
List<HrmFlowActionTimelineVo> timelines = new ArrayList<HrmFlowActionTimelineVo>();
for (HrmFlowAction action : actions) {
HrmFlowActionTimelineVo vo = new HrmFlowActionTimelineVo();
vo.setActionId(action.getActionId());
vo.setTaskId(action.getTaskId());
vo.setInstId(action.getInstId());
vo.setActionUserId(action.getActionUserId());
vo.setActionUserName(action.getActionUserId() == null ? null : userService.selectNickNameById(action.getActionUserId()));
vo.setAction(action.getAction());
vo.setActionText(actionText(action.getAction()));
vo.setRemark(action.getRemark());
vo.setBizType(action.getBizType());
vo.setBizId(action.getBizId());
HrmFlowTask task = taskMap.get(action.getTaskId());
if (task != null) {
vo.setNodeId(task.getNodeId());
vo.setTaskStatus(task.getStatus());
HrmFlowNode node = task.getNodeId() == null ? null : nodeMapper.selectById(task.getNodeId());
if (node != null) {
vo.setNodeName(node.getRemark());
}
}
vo.setCreateTime(action.getCreateTime());
timelines.add(vo);
}
return timelines;
}
private String actionText(String action) {
if (action == null) {
return "-";
}
String lower = action.toLowerCase();
if ("approve".equals(lower)) {
return "通过";
}
if ("reject".equals(lower)) {
return "驳回";
}
if ("withdraw".equals(lower)) {
return "撤回";
}
if ("transfer".equals(lower)) {
return "转办";
}
if ("stamp".equals(lower)) {
return "盖章";
}
return action;
}
private LambdaQueryWrapper<HrmFlowTask> buildQueryWrapper(HrmFlowTaskBo bo) {
LambdaQueryWrapper<HrmFlowTask> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getTaskId() != null, HrmFlowTask::getTaskId, bo.getTaskId());
@@ -416,6 +556,11 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
if (result.getRecords() != null && !result.getRecords().isEmpty()) {
fillBizData(result.getRecords());
result.getRecords().forEach(task -> {
if (task.getAssigneeUserId() != null) {
task.setAssigneeNickName(userService.selectNickNameById(task.getAssigneeUserId()));
}
});
}
return TableDataInfo.build(result);

View File

@@ -0,0 +1,232 @@
package com.ruoyi.hrm.service.impl;
import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
import com.ruoyi.oss.factory.OssFactory;
import com.ruoyi.system.mapper.SysOssMapper;
import com.ruoyi.system.domain.vo.SysOssVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 发票OCR识别服务实现调用Python OCR微服务
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class HrmInvoiceOcrServiceImpl implements IHrmInvoiceOcrService {
@Value("${fad.ocr.url}")
String ocrUrl;
@Value("${fad.ocr.api-key}")
String apiKey;
private final SysOssMapper sysOssMapper;
@Override
public HrmInvoiceOcrResultVo recognizeByOssId(Long ossId) {
SysOssVo oss = sysOssMapper.selectVoById(ossId);
if (oss == null) {
throw new ServiceException("附件不存在: " + ossId);
}
byte[] fileBytes;
try (InputStream in = OssFactory.instance().getObjectContent(oss.getUrl())) {
fileBytes = IoUtil.readBytes(in);
} catch (Exception e) {
throw new ServiceException("读取附件失败: " + e.getMessage());
}
String fileName = StringUtils.defaultIfBlank(oss.getOriginalName(), oss.getFileName());
return callOcrService(fileBytes, fileName, oss.getFileSuffix());
}
private HrmInvoiceOcrResultVo callOcrService(byte[] fileBytes, String fileName, String fileSuffix) {
if (StringUtils.isBlank(ocrUrl)) {
throw new ServiceException("OCR服务地址未配置请检查 fad.ocr.url");
}
// 推断 content-type
String suffix = StringUtils.defaultIfBlank(fileSuffix, "").toLowerCase().replace(".", "");
String contentType;
switch (suffix) {
case "pdf":
contentType = "application/pdf";
break;
case "png":
contentType = "image/png";
break;
case "jpg":
case "jpeg":
contentType = "image/jpeg";
break;
default:
contentType = "application/octet-stream";
}
// 构建 multipart 请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
if (StringUtils.isNotBlank(apiKey)) {
headers.set("X-API-Key", apiKey);
}
final String finalContentType = contentType;
final String finalFileName = StringUtils.defaultIfBlank(fileName, "invoice" + "." + suffix);
ByteArrayResource fileResource = new ByteArrayResource(fileBytes) {
@Override
public String getFilename() {
return finalFileName;
}
};
HttpHeaders fileHeaders = new HttpHeaders();
fileHeaders.setContentType(MediaType.parseMediaType(finalContentType));
HttpEntity<ByteArrayResource> filePart = new HttpEntity<>(fileResource, fileHeaders);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", filePart);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response;
try {
response = restTemplate.postForEntity(ocrUrl + "/v1/invoice/ocr", requestEntity, String.class);
} catch (Exception e) {
log.error("[OCR] 调用OCR服务失败 url={} error={}", ocrUrl, e.getMessage());
throw new ServiceException("OCR服务调用失败: " + e.getMessage());
}
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
throw new ServiceException("OCR服务返回异常: " + response.getStatusCode());
}
return parseOcrResponse(response.getBody());
}
private HrmInvoiceOcrResultVo parseOcrResponse(String responseBody) {
HrmInvoiceOcrResultVo result = new HrmInvoiceOcrResultVo();
try {
JSONObject root = JSON.parseObject(responseBody);
JSONObject data = root.getJSONObject("data");
if (data == null) {
return result;
}
result.setInvoiceType(getFieldValue(data, "invoice_type"));
result.setSellerName(getFieldValue(data, "seller_name"));
result.setInvoiceDate(getFieldValue(data, "invoice_date"));
String totalAmountStr = getFieldValue(data, "amount_with_tax");
if (StringUtils.isBlank(totalAmountStr)) {
totalAmountStr = getFieldValue(data, "total_amount");
}
result.setTotalAmount(parseBigDecimal(totalAmountStr));
// 解析明细行
List<HrmInvoiceOcrResultVo.Item> items = new ArrayList<>();
JSONArray lineItems = data.getJSONArray("line_items");
if (lineItems != null) {
for (int i = 0; i < lineItems.size(); i++) {
JSONObject li = lineItems.getJSONObject(i);
if (li == null) continue;
String name = getStringOrFieldValue(li, "project_name");
if (StringUtils.isBlank(name)) continue;
HrmInvoiceOcrResultVo.Item item = new HrmInvoiceOcrResultVo.Item();
item.setItemName(name);
// 行项目金额取价税合计:税前金额 + 税额
BigDecimal preAmt = parseBigDecimal(getStringOrFieldValue(li, "amount"));
BigDecimal taxAmt = parseBigDecimal(getStringOrFieldValue(li, "tax_amount"));
BigDecimal withTax = null;
if (preAmt != null || taxAmt != null) {
withTax = (preAmt != null ? preAmt : BigDecimal.ZERO)
.add(taxAmt != null ? taxAmt : BigDecimal.ZERO);
}
item.setAmount(withTax != null ? withTax : preAmt);
item.setTaxRate(getStringOrFieldValue(li, "tax_rate"));
items.add(item);
}
}
// 如果没有明细行但有总金额,生成一个汇总条目
if (items.isEmpty() && result.getTotalAmount() != null) {
HrmInvoiceOcrResultVo.Item item = new HrmInvoiceOcrResultVo.Item();
String seller = StringUtils.defaultIfBlank(result.getSellerName(), "发票款项");
item.setItemName(seller);
item.setAmount(result.getTotalAmount());
items.add(item);
}
result.setItems(items);
} catch (Exception e) {
log.warn("[OCR] 解析OCR响应失败: {}", e.getMessage());
}
return result;
}
/** 从 {value: "...", confidence: 0.9} 结构取值,或直接取字符串 */
private String getFieldValue(JSONObject obj, String key) {
Object val = obj.get(key);
if (val == null) return null;
if (val instanceof JSONObject) {
return ((JSONObject) val).getString("value");
}
return val.toString();
}
@Override
public boolean isAlive() {
if (StringUtils.isBlank(ocrUrl)) return false;
try {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> resp = restTemplate.getForEntity(ocrUrl + "/health", String.class);
return resp.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
log.warn("[OCR] 健康检查失败: {}", e.getMessage());
return false;
}
}
private String getStringOrFieldValue(JSONObject obj, String key) {
Object val = obj.get(key);
if (val == null) return null;
if (val instanceof JSONObject) {
return ((JSONObject) val).getString("value");
}
return val.toString();
}
private BigDecimal parseBigDecimal(String raw) {
if (StringUtils.isBlank(raw)) return null;
try {
return new BigDecimal(raw.replace(",", "").replace("¥", "").replace("", "").trim());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -8,11 +8,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.hrm.domain.HrmFlowTemplate;
import com.ruoyi.hrm.domain.HrmInvoiceItem;
import com.ruoyi.hrm.domain.HrmReimburseReq;
import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
import com.ruoyi.hrm.domain.bo.HrmInvoiceItemBo;
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
import com.ruoyi.hrm.mapper.HrmFlowTemplateMapper;
import com.ruoyi.hrm.mapper.HrmInvoiceItemMapper;
import com.ruoyi.hrm.mapper.HrmReimburseReqMapper;
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
import com.ruoyi.hrm.service.IHrmReimburseReqService;
@@ -30,10 +33,18 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
private final HrmReimburseReqMapper baseMapper;
private final HrmFlowTemplateMapper flowTemplateMapper;
private final IHrmFlowInstanceService flowInstanceService;
private final HrmInvoiceItemMapper invoiceItemMapper;
@Override
public HrmReimburseReqVo queryById(Long bizId) {
return baseMapper.selectVoWithProjectById(bizId);
HrmReimburseReqVo vo = baseMapper.selectVoWithProjectById(bizId);
if (vo != null) {
vo.setInvoiceItems(invoiceItemMapper.selectList(Wrappers.<HrmInvoiceItem>lambdaQuery()
.eq(HrmInvoiceItem::getBizType, "reimburse")
.eq(HrmInvoiceItem::getBizId, bizId)
.orderByAsc(HrmInvoiceItem::getSortNo)));
}
return vo;
}
@Override
@@ -61,6 +72,12 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
boolean ok = baseMapper.insert(add) > 0;
HrmReimburseReqVo bean = BeanUtil.toBean(add, HrmReimburseReqVo.class);
// 保存发票条目
if (ok && bo.getInvoiceItems() != null && !bo.getInvoiceItems().isEmpty()) {
saveInvoiceItems("reimburse", add.getBizId(), bo.getInvoiceItems());
}
if (ok && "pending".equalsIgnoreCase(add.getStatus())) {
Long startUserId = LoginHelper.getUserId();
@@ -105,7 +122,11 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(HrmReimburseReqBo bo) {
HrmReimburseReq update = BeanUtil.toBean(bo, HrmReimburseReq.class);
return baseMapper.updateById(update) > 0;
boolean updated = baseMapper.updateById(update) > 0;
if (updated && bo.getInvoiceItems() != null) {
saveInvoiceItems("reimburse", bo.getBizId(), bo.getInvoiceItems());
}
return updated;
}
@Override
@@ -124,6 +145,25 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
return lqw;
}
private void saveInvoiceItems(String bizType, Long bizId, List<HrmInvoiceItemBo> boList) {
// 先清除旧数据,再插入新数据(更新场景兼容)
invoiceItemMapper.delete(Wrappers.<HrmInvoiceItem>lambdaQuery()
.eq(HrmInvoiceItem::getBizType, bizType)
.eq(HrmInvoiceItem::getBizId, bizId));
for (int i = 0; i < boList.size(); i++) {
HrmInvoiceItemBo bo = boList.get(i);
HrmInvoiceItem item = new HrmInvoiceItem();
item.setBizType(bizType);
item.setBizId(bizId);
item.setOssId(bo.getOssId());
item.setSortNo(bo.getSortNo() != null ? bo.getSortNo() : i);
item.setItemName(bo.getItemName());
item.setReason(bo.getReason());
item.setAmount(bo.getAmount());
invoiceItemMapper.insert(item);
}
}
private String defaultStatus(String status) {
return status == null ? "draft" : status;
}

View File

@@ -21,6 +21,8 @@ import com.ruoyi.hrm.service.IHrmSealReqService;
import com.ruoyi.oss.core.OssClient;
import com.ruoyi.oss.entity.UploadResult;
import com.ruoyi.oss.factory.OssFactory;
import com.ruoyi.system.domain.vo.SysOssVo;
import com.ruoyi.system.mapper.SysOssMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -28,7 +30,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -48,10 +49,59 @@ public class HrmSealReqServiceImpl implements IHrmSealReqService {
private final HrmSealReqMapper baseMapper;
private final StampProperties stampProperties;
private final IHrmFlowInstanceService flowInstanceService;
private final SysOssMapper sysOssMapper;
@Override
public HrmSealReqVo queryById(Long bizId) {
return baseMapper.selectVoWithProjectById(bizId);
HrmSealReqVo vo = baseMapper.selectVoWithProjectById(bizId);
if (vo != null) {
vo.setPdfPageTotal(queryPdfPageTotal(bizId));
}
return vo;
}
@Override
public Integer queryPdfPageTotal(Long bizId) {
HrmSealReqVo vo = baseMapper.selectVoWithProjectById(bizId);
String applyFileIds = vo != null ? vo.getApplyFileIds() : null;
if (applyFileIds == null || applyFileIds.trim().isEmpty()) {
return 0;
}
String firstFileId = applyFileIds.split(",")[0].trim();
if (firstFileId.isEmpty()) {
return 0;
}
String fileUrl = resolveSealPdfUrl(firstFileId);
if (fileUrl == null || fileUrl.trim().isEmpty()) {
return 0;
}
try (InputStream inputStream = getObject(fileUrl); PDDocument document = PDDocument.load(inputStream)) {
return document.getNumberOfPages();
} catch (Exception e) {
log.warn("查询用印PDF页数失败 bizId={}, fileId={}, fileUrl={}", bizId, firstFileId, fileUrl, e);
return 0;
}
}
private String resolveObjectKey(String fileRef) {
if (fileRef == null) {
return null;
}
String ref = fileRef.trim();
if (ref.isEmpty()) {
return ref;
}
if (ref.startsWith("http://") || ref.startsWith("https://")) {
int idx = ref.indexOf("/files/");
if (idx >= 0) {
return ref.substring(idx + "/files/".length());
}
idx = ref.indexOf("/files%2F");
if (idx >= 0) {
return ref.substring(idx + "/files%2F".length());
}
}
return ref;
}
@Override
@@ -269,6 +319,17 @@ public class HrmSealReqServiceImpl implements IHrmSealReqService {
}
}
private String resolveSealPdfUrl(String ossIdText) {
try {
Long ossId = Long.valueOf(ossIdText);
SysOssVo sysOss = sysOssMapper.selectVoById(ossId);
return sysOss != null ? sysOss.getUrl() : null;
} catch (Exception e) {
log.warn("解析用印PDF文件地址失败 ossIdText={}", ossIdText, e);
return null;
}
}
private InputStream getObject(String url) {
OssClient storage = OssFactory.instance();
return storage.getObjectContent(url);

View File

@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Date;
@RequiredArgsConstructor
@Service
@@ -128,4 +129,36 @@ public class HrmTravelReqServiceImpl implements IHrmTravelReqService {
private String defaultStatus(String status) {
return status == null ? "draft" : status;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int earlyEnd(Long bizId) {
HrmTravelReq travelReq = baseMapper.selectById(bizId);
if (travelReq == null) {
throw new RuntimeException("出差申请不存在");
}
String status = travelReq.getStatus();
if (!"approved".equals(status) && !"in_progress".equals(status)) {
throw new RuntimeException("当前状态不能提前结束,只有已通过或进行中的出差才能提前结束");
}
// 3. 检查是否已经提前结束过
if (travelReq.getActualEndTime() != null) {
throw new RuntimeException("该出差已经提前结束过了");
}
// 4. 更新实际结束时间为当前时间
travelReq.setActualEndTime(new Date());
// 5. 可选:更新状态为已完成
travelReq.setStatus("completed");
// 6. 执行更新
int result = baseMapper.updateById(travelReq);
if (result <= 0) {
throw new RuntimeException("提前结束失败");
}
return result;
}
}

View File

@@ -51,12 +51,13 @@
<sql id="selectVo">
SELECT
e.emp_id, e.user_id, e.emp_no, e.emp_name, e.gender, e.mobile, e.email, e.id_no,
e.hire_date, e.employment_type, e.status, e.dept_id, e.post_id, e.remark,
e.hire_date, e.employment_type, e.status, su.dept_id, e.post_id, e.remark,
e.create_by, e.create_time, e.update_by, e.update_time,
d.dept_name,
p.post_name
FROM hrm_employee e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id AND d.del_flag = '0'
left join sys_user su on su.user_id = e.user_id
LEFT JOIN sys_dept d ON su.dept_id = d.dept_id AND d.del_flag = 0
LEFT JOIN sys_post p ON e.post_id = p.post_id
WHERE e.del_flag = 0
</sql>

View File

@@ -170,6 +170,8 @@ security:
- /oa/attendanceRecord/**
- /oa/oaWarehouse/**
- /oa/oaWarehouseMaster/**
# 高德逆地理(经纬度转城市等,供前端/H5 调用)
- /oa/amap/**
# MyBatisPlus配置
# https://baomidou.com/config/
@@ -318,7 +320,16 @@ flowable:
# 关闭历史任务定时任务job
async-history-executor-activate: false
fad:
amap:
# 留作后端接口调用(服务 API的 Key
key: 978ae5bc551f57d172d3e397af5a6f67
# 新增的前端 Web 端使用的 Key 和安全密钥
webKey: 34bf20d1db5b183558b9bb85d6eed783
securityKey: 6f9171724396deb5f8c42ef256b3cbc5
ocr:
# 发票OCR服务地址ai-ocr Python服务
url: http://127.0.0.1:8810
# OCR服务 API Key
api-key: change-me-debug-key

View File

@@ -2,13 +2,20 @@ package com.ruoyi.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
private static final int CONNECT_TIMEOUT_MS = 5_000;
private static final int READ_TIMEOUT_MS = 15_000;
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(CONNECT_TIMEOUT_MS);
factory.setReadTimeout(READ_TIMEOUT_MS);
return new RestTemplate(factory);
}
}

View File

@@ -68,11 +68,20 @@
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>fad-hrm</artifactId>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.29</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,46 @@
package com.ruoyi.oa.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
import com.ruoyi.oa.service.IOaAmapGeocodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
/**
* 高德地图:经纬度逆地理编码(城市名等)
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/amap")
public class OaAmapController extends BaseController {
private final IOaAmapGeocodeService oaAmapGeocodeService;
/**
* 根据经纬度获取城市名称(高德逆地理编码)
*/
@GetMapping("/city")
public R<AmapCityNameVo> cityByLocation(
@NotNull(message = "经度不能为空")
@DecimalMin(value = "-180.0", message = "经度范围无效")
@DecimalMax(value = "180.0", message = "经度范围无效")
@RequestParam Double longitude,
@NotNull(message = "纬度不能为空")
@DecimalMin(value = "-90.0", message = "纬度范围无效")
@DecimalMax(value = "90.0", message = "纬度范围无效")
@RequestParam Double latitude
) {
AmapCityNameVo vo = oaAmapGeocodeService.reverseGeocodeCity(longitude, latitude);
return R.ok(vo);
}
}

View File

@@ -0,0 +1,62 @@
package com.ruoyi.oa.controller;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.RepeatSubmit;
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.page.TableDataInfo;
import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.oa.domain.bo.OaCityBo;
import com.ruoyi.oa.domain.vo.OaCityVo;
import com.ruoyi.oa.service.IOaCityService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/city")
@Validated
public class OaCityController extends BaseController {
private final IOaCityService cityService;
@GetMapping("/list")
public TableDataInfo<OaCityVo> list(OaCityBo bo, PageQuery pageQuery) {
return cityService.queryPageList(bo, pageQuery);
}
@GetMapping("/{cityId}")
public R<OaCityVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long cityId) {
return R.ok(cityService.queryById(cityId));
}
@Log(title = "城市管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping
public R<Void> add(@Validated(AddGroup.class) @RequestBody OaCityBo bo) {
return toAjax(cityService.insertByBo(bo));
}
@Log(title = "城市管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
public R<Void> edit(@Validated(EditGroup.class) @RequestBody OaCityBo bo) {
return toAjax(cityService.updateByBo(bo));
}
@Log(title = "城市管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{cityIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] cityIds) {
return toAjax(cityService.deleteWithValidByIds(Arrays.asList(cityIds), true));
}
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.oa.controller;
import com.ruoyi.common.annotation.Log;
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.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.oa.domain.bo.OaProjectOperationLogBo;
import com.ruoyi.oa.domain.vo.OaProjectOperationLogVo;
import com.ruoyi.oa.service.IOaProjectOperationLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 项目操作历史记录
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/projectOperationLog")
public class OaProjectOperationLogController extends BaseController {
private final IOaProjectOperationLogService iOaProjectOperationLogService;
/**
* 分页查询操作历史
*/
@GetMapping("/list")
public TableDataInfo<OaProjectOperationLogVo> list(OaProjectOperationLogBo bo, PageQuery pageQuery) {
return iOaProjectOperationLogService.queryPageList(bo, pageQuery);
}
/**
* 导出操作历史
*/
@Log(title = "项目操作历史", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(OaProjectOperationLogBo bo, HttpServletResponse response) {
List<OaProjectOperationLogVo> list = iOaProjectOperationLogService.queryList(bo);
ExcelUtil.exportExcel(list, "项目操作历史", OaProjectOperationLogVo.class, response);
}
/**
* 查询单条详情
*/
@GetMapping("/{logId}")
public R<OaProjectOperationLogVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long logId) {
return R.ok(iOaProjectOperationLogService.queryById(logId));
}
}

View File

@@ -12,6 +12,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
@@ -51,6 +52,15 @@ public class OaProjectReportController extends BaseController {
return iOaProjectReportService.queryPageList(bo, pageQuery);
}
@GetMapping("/travel-compare")
public TableDataInfo<OaProjectTravelCompareVo> travelCompare(@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate start,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate end,
@RequestParam(required = false) String nickName,
@RequestParam(required = false) String workPlace,
PageQuery pageQuery) {
return iOaProjectReportService.getTravelCompareList(start, end, nickName, workPlace, pageQuery);
}
/**
* 查询项目报工列表
*/

View File

@@ -0,0 +1,49 @@
package com.ruoyi.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 城市管理对象 oa_city
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("oa_city")
public class OaCity extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "city_id")
private Long cityId;
/** 国家 */
private String countryName;
/** 城市 */
private String cityName;
/**
* 城市编码
*/
private String cityCode;
/**
* 所属省份
*/
private String provinceName;
/** 状态 1正常 0禁用 */
private Long status;
/** 备注 */
private String remark;
@TableLogic
private Integer delFlag;
}

View File

@@ -0,0 +1,61 @@
package com.ruoyi.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 项目操作历史记录对象 oa_project_operation_log
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("oa_project_operation_log")
public class OaProjectOperationLog extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "log_id")
private Long logId;
/** 所属项目ID */
private Long projectId;
/**
* 操作对象类型: 1-项目进度 2-进度步骤 3-任务 4-延期申请
*/
private Integer targetType;
/** 操作对象ID */
private Long targetId;
/** 操作对象名称 */
private String targetName;
/**
* 操作类型: 1-新增 2-修改 3-删除 4-状态变更 5-完成 6-申请延期 7-审批通过 8-审批驳回
*/
private Integer operationType;
/** 操作描述 */
private String operationDesc;
/** 操作前快照(JSON) */
private String beforeValue;
/** 操作后快照(JSON) */
private String afterValue;
/** 操作人昵称 */
private String operator;
/** 操作时间 */
private Date operateTime;
@TableLogic(value = "0", delval = "1")
private Integer delFlag;
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.oa.domain.bo;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 城市管理业务对象 oa_city
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OaCityBo extends BaseEntity {
private Long cityId;
@NotBlank(message = "国家不能为空")
private String countryName;
@NotBlank(message = "城市不能为空")
private String cityName;
private Long status;
private String cityCode;
private String provinceName;
private String remark;
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.oa.domain.bo;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 项目操作历史记录 业务对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OaProjectOperationLogBo extends BaseEntity {
/** 主键 */
private Long logId;
/** 所属项目ID */
private Long projectId;
/** 操作对象类型: 1-项目进度 2-进度步骤 3-任务 4-延期申请 */
private Integer targetType;
/** 操作对象ID */
private Long targetId;
/** 操作对象名称 */
private String targetName;
/** 操作类型: 1-新增 2-修改 3-删除 4-状态变更 5-完成 6-申请延期 7-审批通过 8-审批驳回 */
private Integer operationType;
/** 操作描述 */
private String operationDesc;
/** 操作前快照(JSON) */
private String beforeValue;
/** 操作后快照(JSON) */
private String afterValue;
/** 操作人昵称 */
private String operator;
/** 操作时间范围-开始 */
private Date operateTimeStart;
/** 操作时间范围-结束 */
private Date operateTimeEnd;
}

View File

@@ -207,6 +207,11 @@ public class SysOaProjectBo extends BaseEntity {
*/
private String keyword;
/**
* 为 true 时在列表结果中附带各项目进度步骤统计(综合看板等,不参与 SQL 条件)
*/
private Boolean scheduleStats;
//是否置顶
private Integer isTop;

View File

@@ -0,0 +1,18 @@
package com.ruoyi.oa.domain.dto;
import lombok.Data;
/**
* 项目维度进度步骤汇总(综合看板左侧列表等)
*/
@Data
public class ProjectScheduleStepStatsDto {
private Long projectId;
private Long totalNodes;
private Long completedNodes;
private Long pendingAcceptNodes;
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 高德逆地理编码:城市名称(及可选行政区信息,便于展示/调试)
*/
@Data
public class AmapCityNameVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 城市名(直辖市、省直辖等场景可能为省名或区名,与高德 addressComponent 一致) */
private String cityName;
/** 省 */
private String province;
/** 区/县 */
private String district;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 发票识别明细行
*/
@Data
public class InvoiceOcrItemVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 货物或应税劳务、服务名称 */
private String itemName;
/** 金额 */
private BigDecimal amount;
/** 税率 */
private String taxRate;
/** 税额 */
private BigDecimal taxAmount;
}

View File

@@ -0,0 +1,56 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 发票识别结果
*/
@Data
public class InvoiceOcrResultVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 文件名 */
private String fileName;
/** 发票类型 */
private String invoiceType;
/** 发票代码 */
private String invoiceCode;
/** 发票号码 */
private String invoiceNumber;
/** 开票日期 */
private String invoiceDate;
/** 购买方名称 */
private String buyerName;
/** 销售方名称 */
private String sellerName;
/** 不含税金额 */
private BigDecimal amountWithoutTax;
/** 税额 */
private BigDecimal taxAmount;
/** 价税合计 */
private BigDecimal totalAmount;
/** 发票内容摘要 */
private String contentSummary;
/** 提取到的原始文本 */
private String rawText;
/** 明细 */
private List<InvoiceOcrItemVo> items = new ArrayList<>();
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.oa.domain.vo;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 城市管理视图对象 oa_city
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OaCityVo extends BaseEntity {
private Long cityId;
private String countryName;
private String cityName;
private Long status;
private String remark;
/** 城市编码 */
private String cityCode;
/** 所属省份 */
private String provinceName;
}

View File

@@ -0,0 +1,59 @@
package com.ruoyi.oa.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* 项目操作历史记录 视图对象
*/
@Data
@ExcelIgnoreUnannotated
public class OaProjectOperationLogVo {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "主键ID")
private Long logId;
@ExcelProperty(value = "所属项目ID")
private Long projectId;
@ExcelProperty(value = "项目名称")
private String projectName;
@ExcelProperty(value = "项目编号")
private String projectNum;
/** 操作对象类型: 1-项目进度 2-进度步骤 3-任务 4-延期申请 */
@ExcelProperty(value = "操作对象类型")
private Integer targetType;
@ExcelProperty(value = "操作对象ID")
private Long targetId;
@ExcelProperty(value = "操作对象名称")
private String targetName;
/** 操作类型: 1-新增 2-修改 3-删除 4-状态变更 5-完成 6-申请延期 7-审批通过 8-审批驳回 */
@ExcelProperty(value = "操作类型")
private Integer operationType;
@ExcelProperty(value = "操作描述")
private String operationDesc;
private String beforeValue;
private String afterValue;
@ExcelProperty(value = "操作人")
private String operator;
@ExcelProperty(value = "操作时间")
private Date operateTime;
@ExcelProperty(value = "创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.oa.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 报工出差比对结果
*/
@Data
public class OaProjectTravelCompareVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 用户昵称 */
private String nickName;
/** 比对日期(一天) */
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate compareDate;
/** 是否出差 */
private Long isTrip;
/** 是否出差(中文) */
private String trip;
/** 报工地点 */
private String workPlace;
/** 实际出差地点 */
private String travelPlace;
/** 机器比对结果 */
private String compareResult;
/** 是否通过 */
private Boolean pass;
/** 出差记录ID */
private Long travelBizId;
/** 报工ID */
private Long reportId;
}

View File

@@ -6,6 +6,7 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.annotation.ExcelDictFormat;
import com.ruoyi.common.convert.ExcelDictConvert;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
@@ -19,7 +20,7 @@ import java.util.Date;
*/
@Data
@ExcelIgnoreUnannotated
public class OaRequirementsVo {
public class OaRequirementsVo extends BaseEntity {
private static final long serialVersionUID = 1L;

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.annotation.ExcelDictFormat;
@@ -14,7 +15,6 @@ import com.ruoyi.system.domain.SysOss;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@@ -284,4 +284,27 @@ public class SysOaProjectVo {
private Long processCardCount;
private Long deliveryOrderCount;
/** 列表 SQL 聚合:任务/进度总览 */
@ExcelIgnore
private Long taskFinishCount;
@ExcelIgnore
private Long taskTotalCount;
@ExcelIgnore
private Long scheduleTotalCount;
@ExcelIgnore
private Long scheduleFinishCount;
/** 进度步骤汇总(列表请求 scheduleStats=true 时由服务层填充) */
@ExcelIgnore
private Long scheduleStepTotal;
@ExcelIgnore
private Long scheduleStepCompleted;
@ExcelIgnore
private Long scheduleStepPendingAccept;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.oa.mapper;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.oa.domain.OaCity;
import com.ruoyi.oa.domain.vo.OaCityVo;
/**
* 城市管理 Mapper
*/
public interface OaCityMapper extends BaseMapperPlus<OaCityMapper, OaCity, OaCityVo> {
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.oa.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.oa.domain.OaProjectOperationLog;
import com.ruoyi.oa.domain.vo.OaProjectOperationLogVo;
import org.apache.ibatis.annotations.Param;
/**
* 项目操作历史记录 Mapper 接口
*/
public interface OaProjectOperationLogMapper extends BaseMapperPlus<OaProjectOperationLogMapper, OaProjectOperationLog, OaProjectOperationLogVo> {
Page<OaProjectOperationLogVo> selectVoPagePlus(Page<Object> page, @Param(Constants.WRAPPER) QueryWrapper<OaProjectOperationLog> qw);
}

View File

@@ -52,5 +52,11 @@ public interface OaProjectReportMapper extends BaseMapperPlus<OaProjectReportMap
List<OaProjectReportVo> getClearList(@Param("start") LocalDate start, @Param("end") LocalDate end);
Page<OaProjectReportVo> selectTravelCompareReportPage(@Param("page") Page<OaProjectReportVo> page,
@Param("start") LocalDate start,
@Param("end") LocalDate end,
@Param("nickName") String nickName,
@Param("workPlace") String workPlace);
List<OaProjectReportVo> getSummaryData(@Param("start") LocalDate start, @Param("end") LocalDate end);
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.oa.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.oa.domain.OaProjectScheduleStep;
import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto;
import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
@@ -44,4 +45,9 @@ public interface OaProjectScheduleStepMapper extends BaseMapperPlus<OaProjectSch
int deleteByScheduleIds(@Param("scheduleIds") Collection<Long> scheduleIds);
Page<OaProjectScheduleStepVo> selectVoPageNew(Page<Object> build,@Param(Constants.WRAPPER) QueryWrapper<OaProjectScheduleStep> lqw);
/**
* 按项目汇总进度步骤:总数、已完成(2)、待验收(1)
*/
List<ProjectScheduleStepStatsDto> selectStepStatsGroupByProjectId(@Param("projectIds") Collection<Long> projectIds);
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.oa.service;
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
/**
* 高德地图逆地理编码(经纬度 → 城市等)
*/
public interface IOaAmapGeocodeService {
/**
* 根据经纬度解析城市名称等信息
*
* @param longitude 经度
* @param latitude 纬度
* @return 非 null解析失败时 cityName 等可能为空
*/
AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude);
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.oa.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.oa.domain.bo.OaCityBo;
import com.ruoyi.oa.domain.vo.OaCityVo;
import java.util.Collection;
import java.util.List;
public interface IOaCityService {
OaCityVo queryById(Long cityId);
TableDataInfo<OaCityVo> queryPageList(OaCityBo bo, PageQuery pageQuery);
List<OaCityVo> queryList(OaCityBo bo);
Boolean insertByBo(OaCityBo bo);
Boolean updateByBo(OaCityBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -0,0 +1,39 @@
package com.ruoyi.oa.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.oa.domain.bo.OaProjectOperationLogBo;
import com.ruoyi.oa.domain.vo.OaProjectOperationLogVo;
import java.util.List;
/**
* 项目操作历史记录 Service 接口
*/
public interface IOaProjectOperationLogService {
/** 分页查询 */
TableDataInfo<OaProjectOperationLogVo> queryPageList(OaProjectOperationLogBo bo, PageQuery pageQuery);
/** 列表查询(不分页) */
List<OaProjectOperationLogVo> queryList(OaProjectOperationLogBo bo);
/** 详情 */
OaProjectOperationLogVo queryById(Long logId);
/**
* 记录一条操作日志(供其他 Service 内部调用)
*
* @param projectId 项目ID
* @param targetType 对象类型: 1-进度 2-步骤 3-任务 4-延期
* @param targetId 对象ID
* @param targetName 对象名称
* @param operationType 操作类型: 1-新增 2-修改 3-删除 4-状态变更 5-完成 6-申请延期 7-审批通过 8-审批驳回
* @param operationDesc 可读描述
* @param beforeValue 操作前JSON可为 null
* @param afterValue 操作后JSON可为 null
*/
void recordLog(Long projectId, Integer targetType, Long targetId, String targetName,
Integer operationType, String operationDesc,
String beforeValue, String afterValue);
}

View File

@@ -1,78 +1,48 @@
package com.ruoyi.oa.service;
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
/**
* 项目报工Service接口
*
* @author hdka
* @date 2025-06-16
*/
public interface IOaProjectReportService {
/**
* 查询项目报工
*/
OaProjectReportVo queryById(Long reportId);
/**
* 查询项目报工列表
*/
TableDataInfo<OaProjectReportVo> queryPageList(OaProjectReportBo bo, PageQuery pageQuery);
/**
* 查询项目报工列表
*/
List<OaProjectReportVo> queryList(OaProjectReportBo bo);
/**
* 新增项目报工
*/
Boolean insertByBo(OaProjectReportBo bo);
/**
* 修改项目报工
*/
Boolean updateByBo(OaProjectReportBo bo);
/**
* 校验并批量删除项目报工信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 报工数据看板
* @return
*/
ProjectReportCardVo getCardData();
public List<ProjectReportTrendVo> getTrend(LocalDate start, LocalDate end);
public List<ProjectReportPieVo> getDistribution(LocalDate start, LocalDate end);
List<OaProjectReportVo> getRankData(LocalDate start, LocalDate end);
List<OaProjectReportVo> getProjects(LocalDate start, LocalDate end);
List<OaProjectReportVo> clearList(LocalDate start, LocalDate end);
OaProjectReportVo queryById(Long reportId);
Boolean insertByBo(OaProjectReportBo bo);
Boolean updateByBo(OaProjectReportBo bo);
Boolean deleteWithValidByIds(List<Long> ids, Boolean isValid);
List<OaProjectReportVo> getRankData(LocalDate start, LocalDate end);
ProjectReportCardVo getCardData();
List<ProjectReportTrendVo> getTrend(LocalDate start, LocalDate end);
List<ProjectReportPieVo> getDistribution(LocalDate start, LocalDate end);
List<OaProjectReportVo> getProjects(LocalDate start, LocalDate end);
List<OaProjectReportVo> getSummaryData(LocalDate start, LocalDate end);
/**
* 查询当前登录用户今日的报工记录
*/
OaProjectReportVo getTodayReportByCurrentUser();
Boolean insertReportSupplement(OaProjectReportBo bo);
TableDataInfo<OaProjectTravelCompareVo> getTravelCompareList(LocalDate start, LocalDate end, String nickName, String workPlace, PageQuery pageQuery);
}

View File

@@ -0,0 +1,118 @@
package com.ruoyi.oa.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
import com.ruoyi.oa.service.IOaAmapGeocodeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* 高德逆地理编码restapi.amap.com/v3/geocode/regeo
* <p>
* 配置项:{@code fad.amap.key}(与 {@code application.yml} 中一致)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OaAmapGeocodeServiceImpl implements IOaAmapGeocodeService {
private static final String AMAP_REGEO_URL = "https://restapi.amap.com/v3/geocode/regeo";
private final RestTemplate restTemplate;
@Value("${fad.amap.key:}")
private String amapKey;
@Override
public AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude) {
AmapCityNameVo vo = new AmapCityNameVo();
if (longitude == null || latitude == null) {
return vo;
}
if (!StringUtils.hasText(amapKey)) {
log.warn("fad.amap.key 未配置,无法调用高德逆地理编码");
return vo;
}
try {
// 高德要求location = 经度,纬度
String location = longitude + "," + latitude;
String url = UriComponentsBuilder.fromHttpUrl(AMAP_REGEO_URL)
.queryParam("key", amapKey)
.queryParam("location", location)
.queryParam("extensions", "base")
.queryParam("batch", "false")
.queryParam("output", "JSON")
.build(true)
.toUriString();
String body = restTemplate.getForObject(url, String.class);
if (!StringUtils.hasText(body)) {
log.warn("高德逆地理编码响应为空, location={}", location);
return vo;
}
JSONObject response = JSONObject.parseObject(body);
if (response == null) {
log.warn("高德逆地理编码 JSON 解析失败, location={}", location);
return vo;
}
if (!"1".equals(response.getString("status"))) {
String info = response.getString("info");
log.warn("高德逆地理编码失败: status={}, info={}, infocode={}",
response.getString("status"), info, response.getString("infocode"));
return vo;
}
JSONObject regeocode = response.getJSONObject("regeocode");
if (regeocode == null) {
return vo;
}
JSONObject addressComponent = regeocode.getJSONObject("addressComponent");
if (addressComponent == null) {
return vo;
}
String province = nullToEmpty(addressComponent.getString("province"));
String district = nullToEmpty(addressComponent.getString("district"));
vo.setProvince(province);
vo.setDistrict(district);
String cityName = resolveCityName(addressComponent, province);
vo.setCityName(cityName);
return vo;
} catch (Exception e) {
log.warn("高德逆地理编码异常, longitude={}, latitude={}, err={}",
longitude, latitude, e.getMessage(), e);
return vo;
}
}
/**
* 城市字段:普通城市为字符串;部分省直辖为 [];直辖市可能用省名表示
*/
private static String resolveCityName(JSONObject addressComponent, String province) {
Object cityRaw = addressComponent.get("city");
if (cityRaw instanceof JSONArray) {
JSONArray arr = (JSONArray) cityRaw;
if (!arr.isEmpty()) {
return nullToEmpty(arr.getString(0));
}
return province;
}
String city = addressComponent.getString("city");
if (StringUtils.hasText(city) && !"[]".equals(city)) {
return city.trim();
}
return province;
}
private static String nullToEmpty(String s) {
return s == null ? "" : s.trim();
}
}

View File

@@ -0,0 +1,73 @@
package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.oa.domain.OaCity;
import com.ruoyi.oa.domain.bo.OaCityBo;
import com.ruoyi.oa.domain.vo.OaCityVo;
import com.ruoyi.oa.mapper.OaCityMapper;
import com.ruoyi.oa.service.IOaCityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
@RequiredArgsConstructor
@Service
public class OaCityServiceImpl implements IOaCityService {
private final OaCityMapper baseMapper;
@Override
public OaCityVo queryById(Long cityId) {
return baseMapper.selectVoById(cityId);
}
@Override
public TableDataInfo<OaCityVo> queryPageList(OaCityBo bo, PageQuery pageQuery) {
IPage<OaCityVo> page = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
TableDataInfo<OaCityVo> tableDataInfo = new TableDataInfo<>(page.getRecords(), page.getTotal());
return tableDataInfo;
}
@Override
public List<OaCityVo> queryList(OaCityBo bo) {
return baseMapper.selectVoList(buildQueryWrapper(bo));
}
@Override
public Boolean insertByBo(OaCityBo bo) {
OaCity add = BeanUtil.toBean(bo, OaCity.class);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setCityId(add.getCityId());
}
return flag;
}
@Override
public Boolean updateByBo(OaCityBo bo) {
OaCity update = BeanUtil.toBean(bo, OaCity.class);
return baseMapper.updateById(update) > 0;
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteBatchIds(ids) > 0;
}
private LambdaQueryWrapper<OaCity> buildQueryWrapper(OaCityBo bo) {
LambdaQueryWrapper<OaCity> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getCountryName()), OaCity::getCountryName, bo.getCountryName());
lqw.like(StringUtils.isNotBlank(bo.getCityName()), OaCity::getCityName, bo.getCityName());
lqw.eq(bo.getStatus() != null, OaCity::getStatus, bo.getStatus());
lqw.orderByDesc(OaCity::getCreateTime);
return lqw;
}
}

View File

@@ -0,0 +1,106 @@
package com.ruoyi.oa.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.oa.domain.OaProjectOperationLog;
import com.ruoyi.oa.domain.bo.OaProjectOperationLogBo;
import com.ruoyi.oa.domain.vo.OaProjectOperationLogVo;
import com.ruoyi.oa.mapper.OaProjectOperationLogMapper;
import com.ruoyi.oa.service.IOaProjectOperationLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* 项目操作历史记录 Service 实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class OaProjectOperationLogServiceImpl implements IOaProjectOperationLogService {
private final OaProjectOperationLogMapper baseMapper;
@Override
public TableDataInfo<OaProjectOperationLogVo> queryPageList(OaProjectOperationLogBo bo, PageQuery pageQuery) {
QueryWrapper<OaProjectOperationLog> qw = buildQueryWrapper(bo);
Page<OaProjectOperationLogVo> result = baseMapper.selectVoPagePlus(pageQuery.build(), qw);
return TableDataInfo.build(result);
}
@Override
public List<OaProjectOperationLogVo> queryList(OaProjectOperationLogBo bo) {
QueryWrapper<OaProjectOperationLog> qw = buildQueryWrapper(bo);
return baseMapper.selectVoList(qw);
}
@Override
public OaProjectOperationLogVo queryById(Long logId) {
return baseMapper.selectVoById(logId);
}
@Override
public void recordLog(Long projectId, Integer targetType, Long targetId, String targetName,
Integer operationType, String operationDesc,
String beforeValue, String afterValue) {
try {
OaProjectOperationLog entity = new OaProjectOperationLog();
entity.setProjectId(projectId);
entity.setTargetType(targetType);
entity.setTargetId(targetId);
entity.setTargetName(StringUtils.defaultIfBlank(targetName, ""));
entity.setOperationType(operationType);
entity.setOperationDesc(StringUtils.defaultIfBlank(operationDesc, ""));
entity.setBeforeValue(beforeValue);
entity.setAfterValue(afterValue);
String operator = tryGetNickName();
entity.setOperator(operator);
entity.setOperateTime(new Date());
// @Async 线程中 Sa-Token 上下文已失效MP 自动填充取不到登录用户,手动赋值
entity.setCreateBy(operator);
entity.setUpdateBy(operator);
entity.setCreateTime(new Date());
entity.setUpdateTime(new Date());
baseMapper.insert(entity);
} catch (Exception e) {
log.error("记录操作日志失败: projectId={}, targetType={}, targetId={}", projectId, targetType, targetId, e);
}
}
private String tryGetNickName() {
try {
String name = LoginHelper.getNickName();
return StringUtils.isNotBlank(name) ? name : "system";
} catch (Exception e) {
return "system";
}
}
private QueryWrapper<OaProjectOperationLog> buildQueryWrapper(OaProjectOperationLogBo bo) {
QueryWrapper<OaProjectOperationLog> qw = Wrappers.query();
qw.eq("l.del_flag", 0);
qw.eq(bo.getProjectId() != null, "l.project_id", bo.getProjectId());
qw.eq(bo.getTargetType() != null, "l.target_type", bo.getTargetType());
qw.eq(bo.getTargetId() != null, "l.target_id", bo.getTargetId());
qw.eq(bo.getOperationType() != null, "l.operation_type", bo.getOperationType());
qw.like(StringUtils.isNotBlank(bo.getOperator()), "l.operator", bo.getOperator());
qw.like(StringUtils.isNotBlank(bo.getOperationDesc()), "l.operation_desc", bo.getOperationDesc());
if (bo.getOperateTimeStart() != null) {
qw.ge("l.operate_time", bo.getOperateTimeStart());
}
if (bo.getOperateTimeEnd() != null) {
qw.le("l.operate_time", bo.getOperateTimeEnd());
}
qw.orderByDesc("l.operate_time");
return qw;
}
}

View File

@@ -1,30 +1,35 @@
package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
import com.ruoyi.hrm.service.IHrmTravelReqService;
import com.ruoyi.oa.domain.OaProjectReport;
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
import com.ruoyi.oa.domain.OaProjectReport;
import com.ruoyi.oa.mapper.OaProjectReportMapper;
import com.ruoyi.oa.service.IOaProjectReportService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 项目报工Service业务层处理
@@ -37,6 +42,7 @@ import java.util.*;
public class OaProjectReportServiceImpl implements IOaProjectReportService {
private final OaProjectReportMapper baseMapper;
private final IHrmTravelReqService hrmTravelReqService;
/**
* 查询项目报工
@@ -64,6 +70,77 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
return baseMapper.selectAll(bo);
}
@Override
public TableDataInfo<OaProjectTravelCompareVo> getTravelCompareList(LocalDate start, LocalDate end, String nickName, String workPlace, PageQuery pageQuery) {
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() <= 0 ? 1 : pageQuery.getPageNum();
int pageSize = pageQuery.getPageSize() == null || pageQuery.getPageSize() <= 0 ? 50 : pageQuery.getPageSize();
Page<OaProjectReportVo> page = new Page<>(pageNum, pageSize);
Page<OaProjectReportVo> reportPage = baseMapper.selectTravelCompareReportPage(page, start, end, nickName, workPlace);
List<OaProjectReportVo> reportList = reportPage.getRecords();
List<OaProjectTravelCompareVo> rows = new ArrayList<>();
List<HrmTravelReqVo> travelList = hrmTravelReqService.queryList(new HrmTravelReqBo());
for (OaProjectReportVo report : reportList) {
OaProjectTravelCompareVo vo = new OaProjectTravelCompareVo();
vo.setNickName(report.getNickName());
vo.setCompareDate(toLocalDate(report.getCreateTime()));
vo.setWorkPlace(safeString(report.getWorkPlace()));
vo.setReportId(report.getReportId());
vo.setIsTrip(report.getIsTrip());
vo.setTrip(report.getTrip());
if (report.getIsTrip() == null || report.getIsTrip() != 1) {
vo.setTravelPlace("无出差记录");
vo.setPass(false);
vo.setCompareResult("该员工未出差");
rows.add(vo);
continue;
}
HrmTravelReqVo matchedTravel = null;
LocalDate reportDate = vo.getCompareDate();
if (reportDate != null) {
for (HrmTravelReqVo travel : travelList) {
if (travel.getEmpId() == null || report.getUserId() == null || !travel.getEmpId().equals(report.getUserId())) {
continue;
}
if (travel.getStartTime() == null) {
continue;
}
LocalDate travelStart = toLocalDate(travel.getStartTime());
LocalDate travelEnd = toLocalDate(travel.getActualEndTime() != null ? travel.getActualEndTime() : travel.getEndTime());
if (travelStart == null || travelEnd == null) {
continue;
}
if (!reportDate.isBefore(travelStart) && !reportDate.isAfter(travelEnd)) {
matchedTravel = travel;
break;
}
}
}
if (matchedTravel == null) {
vo.setTravelPlace("无出差记录");
vo.setPass(false);
vo.setCompareResult("该员工未出差");
} else {
String travelPlace = safeString(matchedTravel.getDestination());
boolean pass = isLocationMatched(vo.getWorkPlace(), travelPlace);
vo.setTravelPlace(travelPlace);
vo.setPass(pass);
vo.setCompareResult(pass ? "通过" : "异常");
vo.setTravelBizId(matchedTravel.getBizId());
}
rows.add(vo);
}
TableDataInfo<OaProjectTravelCompareVo> table = TableDataInfo.build();
table.setRows(rows);
table.setTotal(reportPage.getTotal());
return table;
}
private QueryWrapper<OaProjectReport> buildQueryWrapper(OaProjectReportBo bo) {
Map<String, Object> params = bo.getParams();
@@ -110,6 +187,45 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
return lqw;
}
private LocalDate toLocalDate(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
private String safeString(String value) {
return value == null ? "" : value.trim();
}
private boolean isLocationMatched(String a, String b) {
if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
return false;
}
String left = normalizeLocation(a);
String right = normalizeLocation(b);
return !left.isEmpty() && left.equals(right);
}
private String normalizeLocation(String value) {
String normalized = value.trim();
if (normalized.length() >= 2) {
normalized = normalized.substring(0, 2);
}
return normalized;
}
private <T> TableDataInfo<T> toPage(List<T> list, PageQuery pageQuery) {
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() <= 0 ? 1 : pageQuery.getPageNum();
int pageSize = pageQuery.getPageSize() == null || pageQuery.getPageSize() <= 0 ? 50 : pageQuery.getPageSize();
long total = list.size();
int fromIndex = Math.min((pageNum - 1) * pageSize, list.size());
int toIndex = Math.min(fromIndex + pageSize, list.size());
List<T> records = list.subList(fromIndex, toIndex);
TableDataInfo<T> tableDataInfo = TableDataInfo.build();
tableDataInfo.setRows(records);
tableDataInfo.setTotal(total);
return tableDataInfo;
}
/**
* 新增项目报工
*/
@@ -168,7 +284,7 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
* 批量删除项目报工
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
public Boolean deleteWithValidByIds(List<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}

View File

@@ -14,9 +14,12 @@ import com.ruoyi.oa.domain.bo.OaProjectScheduleDelayBo;
import com.ruoyi.oa.domain.vo.OaProjectScheduleDelayVo;
import com.ruoyi.oa.domain.OaProjectScheduleDelay;
import com.ruoyi.oa.mapper.OaProjectScheduleDelayMapper;
import com.ruoyi.oa.service.IOaProjectOperationLogService;
import com.ruoyi.oa.service.IOaProjectScheduleDelayService;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.oa.mapper.OaProjectScheduleMapper;
import com.ruoyi.oa.mapper.OaProjectScheduleStepMapper;
import com.ruoyi.oa.domain.OaProjectSchedule;
import com.ruoyi.oa.domain.OaProjectScheduleStep;
import java.util.List;
@@ -38,6 +41,8 @@ public class OaProjectScheduleDelayServiceImpl implements IOaProjectScheduleDela
private final OaProjectScheduleDelayMapper baseMapper;
private final OaProjectScheduleStepMapper stepMapper;
private final OaProjectScheduleMapper scheduleMapper;
private final IOaProjectOperationLogService operationLogService;
/**
* 查询项目进度步骤延期记录
@@ -118,6 +123,9 @@ public class OaProjectScheduleDelayServiceImpl implements IOaProjectScheduleDela
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setDelayId(add.getDelayId());
Long projectId = resolveProjectIdFromTrack(bo.getTrackId());
operationLogService.recordLog(projectId, 4, add.getDelayId(), "延期申请",
1, "新增延期申请", null, null);
}
return flag;
}
@@ -169,7 +177,13 @@ public class OaProjectScheduleDelayServiceImpl implements IOaProjectScheduleDela
bo.setOriginalEndTime(Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()));
}
}
return insertByBo(bo);
boolean ok = insertByBo(bo);
if (ok) {
Long projectId = resolveProjectIdFromTrack(bo.getTrackId());
operationLogService.recordLog(projectId, 4, bo.getDelayId(), "延期申请",
6, "申请延期,原因: " + bo.getApplyReason(), null, null);
}
return ok;
}
@Override
@@ -182,6 +196,13 @@ public class OaProjectScheduleDelayServiceImpl implements IOaProjectScheduleDela
bo.setDelayStatus(bo.getApproveResult() == 1L ? 1L : 2L);
}
boolean ok = updateByBo(bo);
if (ok) {
OaProjectScheduleDelay current = baseMapper.selectById(bo.getDelayId());
Long projectId = current != null ? resolveProjectIdFromTrack(current.getTrackId()) : null;
int opType = (bo.getApproveResult() != null && bo.getApproveResult() == 1L) ? 7 : 8;
String desc = opType == 7 ? "审批通过延期申请" : "审批驳回延期申请";
operationLogService.recordLog(projectId, 4, bo.getDelayId(), "延期申请", opType, desc, null, null);
}
// // 审批通过后,同步更新步骤的结束时间为申请的预计结束时间
// if (ok && bo.getApproveResult() != null && bo.getApproveResult() == 1L) {
// OaProjectScheduleDelay current = baseMapper.selectById(bo.getDelayId());
@@ -196,4 +217,12 @@ public class OaProjectScheduleDelayServiceImpl implements IOaProjectScheduleDela
// }
return ok;
}
private Long resolveProjectIdFromTrack(Long trackId) {
if (trackId == null) return null;
OaProjectScheduleStep step = stepMapper.selectById(trackId);
if (step == null || step.getScheduleId() == null) return null;
OaProjectSchedule schedule = scheduleMapper.selectById(step.getScheduleId());
return schedule == null ? null : schedule.getProjectId();
}
}

View File

@@ -28,7 +28,9 @@ import com.ruoyi.oa.domain.bo.OaProjectScheduleBo;
import com.ruoyi.oa.domain.vo.OaProjectScheduleVo;
import com.ruoyi.oa.domain.OaProjectSchedule;
import com.ruoyi.oa.mapper.OaProjectScheduleMapper;
import com.ruoyi.oa.service.IOaProjectOperationLogService;
import com.ruoyi.oa.service.IOaProjectScheduleService;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -55,6 +57,10 @@ public class OaProjectScheduleServiceImpl implements IOaProjectScheduleService {
private final IOaProjectScheduleStepService projectScheduleStepService;
@Lazy
@Resource
private IOaProjectOperationLogService operationLogService;
/**
* 查询项目进度
*/
@@ -188,6 +194,10 @@ public class OaProjectScheduleServiceImpl implements IOaProjectScheduleService {
}
projectScheduleStepService.insertByBo(step);
}
if (flag) {
operationLogService.recordLog(bo.getProjectId(), 1, add.getScheduleId(), "项目进度",
1, "绑定项目进度(模板: " + bo.getTemplateId() + "", null, null);
}
return flag;
}
@@ -210,7 +220,12 @@ public class OaProjectScheduleServiceImpl implements IOaProjectScheduleService {
public Boolean updateByBo(OaProjectScheduleBo bo) {
OaProjectSchedule update = BeanUtil.toBean(bo, OaProjectSchedule.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
boolean ok = baseMapper.updateById(update) > 0;
if (ok) {
operationLogService.recordLog(bo.getProjectId(), 1, bo.getScheduleId(), "项目进度",
2, "修改项目进度", null, null);
}
return ok;
}
/**
@@ -228,7 +243,14 @@ public class OaProjectScheduleServiceImpl implements IOaProjectScheduleService {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
//同时在删除对应的项目进度步骤
// 删前先记录日志(拿到 projectId
for (Long scheduleId : ids) {
OaProjectScheduleVo vo = queryById(scheduleId);
if (vo != null) {
operationLogService.recordLog(vo.getProjectId(), 1, scheduleId, "项目进度",
3, "删除项目进度", null, null);
}
}
projectScheduleStepService.deleteByScheduleIds(ids, false);
return baseMapper.deleteBatchIds(ids) > 0;
}
@@ -281,6 +303,8 @@ public class OaProjectScheduleServiceImpl implements IOaProjectScheduleService {
throw new RuntimeException("未知的模板类型,无法插入项目进度节点");
}
Log.info("项目进度节点批量插入成功");
operationLogService.recordLog(bo.getProjectId(), 1, scheduleId, "项目进度",
1, "绑定项目进度(模板类型: " + bo.getTemplateType() + "", null, null);
return true;
}
}

View File

@@ -23,8 +23,12 @@ import com.ruoyi.oa.mapper.OaUserActiveMapper;
import com.ruoyi.oa.mapper.OaExpressQuestionMapper;
import com.ruoyi.oa.mapper.OaRequirementsMapper;
import com.ruoyi.oa.service.IOaExpressService;
import com.ruoyi.oa.service.IOaProjectOperationLogService;
import com.ruoyi.oa.service.ISysOaTaskService;
import com.ruoyi.system.mapper.SysUserMapper;
import org.springframework.context.annotation.Lazy;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.sql.Timestamp;
@@ -80,6 +84,10 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
private final IOaExpressService oaExpressService;
@Lazy
@Resource
private IOaProjectOperationLogService operationLogService;
/**
@@ -174,6 +182,10 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setTrackId(add.getTrackId());
Long projectId = resolveProjectId(bo.getScheduleId());
String nodeName = buildNodeName(bo.getTabNode(), bo.getFirstLevelNode(), bo.getSecondLevelNode());
operationLogService.recordLog(projectId, 2, add.getTrackId(), nodeName,
1, "新增步骤节点: " + nodeName, null, null);
}
return flag;
}
@@ -206,7 +218,42 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
}
// 4. 执行更新
return baseMapper.updateById(update) > 0;
boolean ok = baseMapper.updateById(update) > 0;
if (ok) {
Long projectId = resolveProjectId(original.getScheduleId());
String nodeName = buildNodeName(original.getTabNode(), original.getFirstLevelNode(), original.getSecondLevelNode());
// 判断是否为状态变更
Integer opType = (bo.getStatus() != null && !bo.getStatus().equals(original.getStatus())) ? 4 : 2;
String desc = opType == 4
? "步骤状态变更为: " + resolveStepStatus(bo.getStatus())
: "修改步骤节点: " + nodeName;
operationLogService.recordLog(projectId, 2, trackId, nodeName, opType, desc, null, null);
}
return ok;
}
private Long resolveProjectId(Long scheduleId) {
if (scheduleId == null) return null;
OaProjectSchedule schedule = scheduleMapper.selectById(scheduleId);
return schedule == null ? null : schedule.getProjectId();
}
private static String buildNodeName(String tabNode, String firstLevel, String secondLevel) {
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(tabNode)) sb.append(tabNode);
if (StringUtils.isNotBlank(firstLevel)) sb.append("/").append(firstLevel);
if (StringUtils.isNotBlank(secondLevel)) sb.append("/").append(secondLevel);
return sb.length() > 0 ? sb.toString() : "步骤节点";
}
private static String resolveStepStatus(Long status) {
if (status == null) return "未知";
switch (status.intValue()) {
case 0: return "进行中";
case 1: return "待验收";
case 2: return "已完成";
default: return String.valueOf(status);
}
}
@@ -225,6 +272,15 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
for (Long trackId : ids) {
OaProjectScheduleStep step = baseMapper.selectById(trackId);
if (step != null) {
Long projectId = resolveProjectId(step.getScheduleId());
String nodeName = buildNodeName(step.getTabNode(), step.getFirstLevelNode(), step.getSecondLevelNode());
operationLogService.recordLog(projectId, 2, trackId, nodeName,
3, "删除步骤节点: " + nodeName, null, null);
}
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
@@ -333,6 +389,9 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
// 批量插入数据库
baseMapper.saveBatch(entities);
Long projectId = resolveProjectId(scheduleId);
operationLogService.recordLog(projectId, 2, scheduleId, "进度步骤",
1, "批量新增步骤节点,共 " + entities.size() + "", null, null);
}
//查询进度负责人负责了多少进度以及完成度是多少0进行中 1待验收 2已完成 是否延期用比较end_time和original_end_time的大小
//如果前者大于后者则表示延期

View File

@@ -16,6 +16,7 @@ import com.ruoyi.oa.domain.bo.OaProjectScheduleBo;
import com.ruoyi.oa.domain.bo.SysOaWarehouseDetailBo;
import com.ruoyi.oa.domain.dto.ProjectActivityDTO;
import com.ruoyi.oa.domain.dto.ProjectDataDTO;
import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto;
import com.ruoyi.oa.domain.vo.*;
import com.ruoyi.oa.service.CodeGeneratorService;
import com.ruoyi.oa.service.IExchangeRateService;
@@ -27,12 +28,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.SysOaProjectBo;
import com.ruoyi.oa.domain.SysOaProject;
import com.ruoyi.oa.mapper.OaProjectScheduleStepMapper;
import com.ruoyi.oa.mapper.SysOaProjectMapper;
import com.ruoyi.oa.service.ISysOaProjectService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -53,6 +56,8 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
private final IOaProjectScheduleStepService oaProjectScheduleStepService;
private final OaProjectScheduleStepMapper oaProjectScheduleStepMapper;
@Autowired
private CodeGeneratorService codeGeneratorService;
@Autowired
@@ -192,6 +197,31 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
}
}
if (Boolean.TRUE.equals(bo.getScheduleStats()) && result.getRecords() != null && !result.getRecords().isEmpty()) {
List<Long> projectIds = result.getRecords().stream()
.map(SysOaProjectVo::getProjectId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (!projectIds.isEmpty()) {
List<ProjectScheduleStepStatsDto> statRows = oaProjectScheduleStepMapper.selectStepStatsGroupByProjectId(projectIds);
Map<Long, ProjectScheduleStepStatsDto> statMap = statRows.stream()
.collect(Collectors.toMap(ProjectScheduleStepStatsDto::getProjectId, Function.identity(), (a, b) -> a));
for (SysOaProjectVo vo : result.getRecords()) {
ProjectScheduleStepStatsDto s = statMap.get(vo.getProjectId());
if (s != null) {
vo.setScheduleStepTotal(s.getTotalNodes());
vo.setScheduleStepCompleted(s.getCompletedNodes());
vo.setScheduleStepPendingAccept(s.getPendingAcceptNodes());
} else {
vo.setScheduleStepTotal(0L);
vo.setScheduleStepCompleted(0L);
vo.setScheduleStepPendingAccept(0L);
}
}
}
}
return TableDataInfo.build(result);
}
private QueryWrapper<SysOaProject> buildAliasPQueryWrapper(SysOaProjectBo bo) {
@@ -221,6 +251,7 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
}
qw.eq(bo.getCustomerId() != null, "p.customer_id", bo.getCustomerId());
qw.orderByDesc("p.is_top").orderByDesc("p.create_time");
qw.groupBy("p.project_id");
return qw;
}

View File

@@ -22,7 +22,11 @@ import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.SysOaTaskBo;
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 org.springframework.context.annotation.Lazy;
import javax.annotation.Resource;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -52,6 +56,10 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
private final SysUserMapper userMapper;
@Lazy
@Resource
private IOaProjectOperationLogService operationLogService;
/**
* 查询任务管理
*/
@@ -154,6 +162,7 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
QueryWrapper<SysOaTask> lqw = Wrappers.query();
lqw.eq("sot.del_flag", 0);
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());
lqw.eq(bo.getTaskId()!=null, "sot.task_id", bo.getTaskId());
lqw.eq(bo.getCreateUserId()!=null, "sot.create_user_id", bo.getCreateUserId());
lqw.eq(bo.getWorkerId()!=null, "sot.worker_id", bo.getWorkerId());
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());
@@ -205,6 +214,10 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
add.setOriginFinishTime(add.getFinishTime());
add.setWorkerId(workerId);
flag = baseMapper.insert(add) > 0;
if (flag) {
operationLogService.recordLog(add.getProjectId(), 3, add.getTaskId(),
add.getTaskTitle(), 1, "新增任务: " + add.getTaskTitle(), null, null);
}
// 判断是否为报工模式
if (bo.getStatus()==1L){
// 这里新增item数据为单个条目
@@ -226,11 +239,24 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
*/
@Override
public Boolean updateByBo(SysOaTaskBo bo) {
// 先删除所有的bo然后再次进行新增操作
SysOaTask update = BeanUtil.toBean(bo, SysOaTask.class);
// 再进行重新新增
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
boolean ok = baseMapper.updateById(update) > 0;
if (ok) {
// BO 可能只带了 taskId如内部调用从 DB 补全 projectId 和 title
Long projectId = bo.getProjectId();
String taskTitle = bo.getTaskTitle();
if (projectId == null || taskTitle == null) {
SysOaTask task = baseMapper.selectById(bo.getTaskId());
if (task != null) {
if (projectId == null) projectId = task.getProjectId();
if (taskTitle == null) taskTitle = task.getTaskTitle();
}
}
operationLogService.recordLog(projectId, 3, bo.getTaskId(),
taskTitle, 2, "修改任务: " + taskTitle, null, null);
}
return ok;
}
/**
@@ -248,6 +274,13 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
for (Long taskId : ids) {
SysOaTask task = baseMapper.selectById(taskId);
if (task != null) {
operationLogService.recordLog(task.getProjectId(), 3, taskId,
task.getTaskTitle(), 3, "删除任务: " + task.getTaskTitle(), null, null);
}
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@@ -258,7 +291,6 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
*/
@Override
public Boolean postponeTask(SysOaTaskBo bo) {
// 防止截止日期被写入 新增对象
SysOaTaskBo sysOaTaskBo = new SysOaTaskBo();
sysOaTaskBo.setTaskId(bo.getTaskId());
sysOaTaskBo.setState(15L);
@@ -269,7 +301,15 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
sysOaTaskItemBo.setTempTime(bo.getTempTime());
taskItemService.updateByBo(sysOaTaskItemBo);
}
return updateByBo(sysOaTaskBo);
boolean ok = updateByBo(sysOaTaskBo);
if (ok) {
SysOaTask task = baseMapper.selectById(bo.getTaskId());
if (task != null) {
operationLogService.recordLog(task.getProjectId(), 3, bo.getTaskId(),
task.getTaskTitle(), 6, "任务申请延期: " + task.getTaskTitle(), null, null);
}
}
return ok;
}
/**
@@ -289,7 +329,12 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
sysOaTaskItem.setEndTime(sysOaTaskItem.getTempTime());
taskItemMapper.updateById(sysOaTaskItem);
}
return baseMapper.updateById(sysOaTask) > 0;
boolean ok = baseMapper.updateById(sysOaTask) > 0;
if (ok) {
operationLogService.recordLog(sysOaTask.getProjectId(), 3, sysOaTask.getTaskId(),
sysOaTask.getTaskTitle(), 7, "任务延期审批通过: " + sysOaTask.getTaskTitle(), null, null);
}
return ok;
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.oa.mapper.OaProjectOperationLogMapper">
<select id="selectVoPagePlus" resultType="com.ruoyi.oa.domain.vo.OaProjectOperationLogVo">
SELECT
l.log_id AS logId,
l.project_id AS projectId,
p.project_name AS projectName,
p.project_num AS projectNum,
l.target_type AS targetType,
l.target_id AS targetId,
l.target_name AS targetName,
l.operation_type AS operationType,
l.operation_desc AS operationDesc,
l.before_value AS beforeValue,
l.after_value AS afterValue,
l.operator AS operator,
l.operate_time AS operateTime,
l.create_time AS createTime
FROM oa_project_operation_log l
LEFT JOIN sys_oa_project p ON p.project_id = l.project_id
${ew.customSqlSegment}
</select>
</mapper>

View File

@@ -195,11 +195,54 @@
left join sys_user su on su.user_id = opr.user_id
left join sys_dept sd on su.dept_id = sd.dept_id
WHERE opr.del_flag = 0
AND DATE(opr.create_time)
BETWEEN DATE_FORMAT(#{start}, '%Y-%m-%d')
AND DATE_FORMAT(#{end}, '%Y-%m-%d')
<if test="start != null">
AND DATE(opr.create_time) &gt;= DATE_FORMAT(#{start}, '%Y-%m-%d')
</if>
<if test="end != null">
AND DATE(opr.create_time) &lt;= DATE_FORMAT(#{end}, '%Y-%m-%d')
</if>
ORDER BY opr.create_time DESC
</select>
<select id="selectTravelCompareReportPage" resultType="com.ruoyi.oa.domain.vo.OaProjectReportVo">
select opr.report_id,
opr.user_id,
opr.is_trip,
opr.work_place,
opr.project_id,
opr.content,
opr.create_time,
opr.create_by,
opr.update_time,
opr.update_by,
opr.del_flag,
opr.remark,
op.project_name,
op.project_num,
op.project_code,
su.nick_name,
opr.work_type,
sd.dept_name
from oa_project_report opr
left join sys_oa_project op on opr.project_id = op.project_id
left join sys_user su on su.user_id = opr.user_id
left join sys_dept sd on su.dept_id = sd.dept_id
<where>
opr.del_flag = 0
<if test="start != null">
AND DATE(opr.create_time) &gt;= DATE_FORMAT(#{start}, '%Y-%m-%d')
</if>
<if test="end != null">
AND DATE(opr.create_time) &lt;= DATE_FORMAT(#{end}, '%Y-%m-%d')
</if>
<if test="nickName != null and nickName != ''">
AND su.nick_name LIKE CONCAT('%', #{nickName}, '%')
</if>
<if test="workPlace != null and workPlace != ''">
AND opr.work_place LIKE CONCAT('%', #{workPlace}, '%')
</if>
</where>
ORDER BY opr.create_time DESC
LIMIT 20
</select>
<select id="getSummaryData" resultType="com.ruoyi.oa.domain.vo.OaProjectReportVo">

View File

@@ -282,5 +282,22 @@
WHERE schedule_id = #{scheduleId}
</select>
<select id="selectStepStatsGroupByProjectId"
resultType="com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto">
SELECT
sch.project_id AS projectId,
COUNT(step.track_id) AS totalNodes,
IFNULL(SUM(CASE WHEN step.status = 2 THEN 1 ELSE 0 END), 0) AS completedNodes,
IFNULL(SUM(CASE WHEN step.status = 1 THEN 1 ELSE 0 END), 0) AS pendingAcceptNodes
FROM oa_project_schedule sch
INNER JOIN oa_project_schedule_step step ON step.schedule_id = sch.schedule_id
WHERE sch.del_flag = '0'
AND step.del_flag = '0'
AND sch.project_id IN
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">
#{pid}
</foreach>
GROUP BY sch.project_id
</select>
</mapper>

View File

@@ -310,9 +310,15 @@
TIMESTAMPDIFF(DAY, NOW(), p.postpone_time) AS remainTime,
p.customer_id AS customerId,
c.name AS customerName,
p.is_top AS isTop
p.is_top AS isTop,
COUNT(sot.task_id) AS taskTotalCount ,
SUM(CASE WHEN sot.status = 1 THEN 1 ELSE 0 END) AS taskFinishCount,
COUNT(ops.schedule_id) AS scheduleTotalCount,
SUM(CASE WHEN ops.status = 2 THEN 1 ELSE 0 END) AS scheduleFinishCount
FROM sys_oa_project p
LEFT JOIN oa_customer c ON p.customer_id = c.customer_id
left join sys_oa_task sot on sot.project_id = p.project_id and sot.del_flag = 0
left join oa_project_schedule ops on ops.project_id = p.project_id and ops.del_flag = 0
${ew.getCustomSqlSegment}
</select>

View File

@@ -5,7 +5,7 @@ VUE_APP_TITLE = 福安德综合办公系统
ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
VUE_APP_BASE_API = '/dev-api'
# VUE_APP_BASE_API = 'http://110.41.139.73:8080'
# 应用访问路径 例如使用前缀 /admin/

View File

@@ -37,6 +37,7 @@
"url": "https://gitee.com/KonBAI-Q/ruoyi-flowable-plus.git"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@babel/parser": "7.7.4",
"@handsontable/vue": "^15.3.0",
"@jiaminghi/data-view": "^2.10.0",

View File

@@ -10,6 +10,7 @@
<title>
<%= webpackConfig.name %>
</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html,
@@ -18,7 +19,7 @@
height: 100%;
margin: 0px;
padding: 0px;
}
.chromeframe {
margin: 0.2em 0;

View File

@@ -57,3 +57,18 @@ export function getAppropriationStats (query) {
params: query
})
}
export function checkAppropriationOcrHealth () {
return request({ url: '/hrm/appropriation/ocr-health', method: 'get' })
}
/**
* 通过ossId触发发票OCR识别返回识别条目不保存
*/
export function ocrAppropriationInvoice (ossId) {
return request({
url: '/hrm/appropriation/ocr-by-oss',
method: 'post',
params: { ossId }
})
}

View File

@@ -16,7 +16,13 @@ export function readCc(ccId) {
method: 'post'
})
}
// 标记抄送为未读
export function unreadCc(ccId) {
return request({
url: `/hrm/flow/cc/${ccId}/unread`,
method: 'post'
})
}
// 手动抄送
export function addCc(data) {
return request({

View File

@@ -254,9 +254,4 @@ export function listAssignTask (instId) {
url: `/hrm/flow/instance/tasks/${instId}`,
method: 'get'
})
/**
* 查询当前用户的审批历史
*/
}

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function listMyApply(query) {
return request({
url: '/hrm/my-apply/list',
method: 'get',
params: query
})
}

View File

@@ -47,3 +47,18 @@ export function allReimburseReq(query) {
})
}
export function checkReimburseOcrHealth() {
return request({ url: '/hrm/reimburse/ocr-health', method: 'get' })
}
/**
* 通过ossId触发发票OCR识别返回识别条目不保存
*/
export function ocrReimburseInvoice(ossId) {
return request({
url: '/hrm/reimburse/ocr-by-oss',
method: 'post',
params: { ossId }
})
}

View File

@@ -46,3 +46,22 @@ export function allTravelReq(query) {
params: query
})
}
export function earlyEndTravel(bizId) {
return request({
url: `/hrm/travel/earlyEnd/${bizId}`,
method: 'put'
})
}
export function getAmapKey() {
return request({
url: '/hrm/travel/amapKey',
method: 'get'
})
}
export function getAmapSecurityKey() {
return request({
url: '/hrm/travel/amapSecurityKey',
method: 'get'
})
}

View File

@@ -0,0 +1,15 @@
import request from '@/utils/request'
/**
* 根据经纬度逆地理编码获取城市等信息(后端转发高德)
* @param {number} longitude 经度
* @param {number} latitude 纬度
*/
export function getCityByLocation (longitude, latitude) {
return request({
url: '/oa/amap/city',
method: 'get',
params: { longitude, latitude },
timeout: 15000
})
}

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
export function listTravelCompare (start, end, nickName, workPlace, pageNum, pageSize) {
return request({
url: '/oa/projectReport/travel-compare',
method: 'get',
params: {
...(start ? { start } : {}),
...(end ? { end } : {}),
...(nickName ? { nickName } : {}),
...(workPlace ? { workPlace } : {}),
pageNum,
pageSize,
orderByColumn: 'compareDate',
isAsc: 'desc'
}
})
}

View File

@@ -0,0 +1,28 @@
import request from '@/utils/request'
// 查询操作历史列表(分页)
export function listOperationLog(query) {
return request({
url: '/oa/projectOperationLog/list',
method: 'get',
params: query
})
}
// 查询操作历史详情
export function getOperationLog(logId) {
return request({
url: '/oa/projectOperationLog/' + logId,
method: 'get'
})
}
// 导出操作历史
export function exportOperationLog(query) {
return request({
url: '/oa/projectOperationLog/export',
method: 'post',
params: query,
responseType: 'blob'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询城市列表
export function listCity(query) {
return request({
url: '/oa/city/list',
method: 'get',
params: query
})
}
// 查询城市详细
export function getCity(cityId) {
return request({
url: `/oa/city/${cityId}`,
method: 'get'
})
}
// 新增城市
export function addCity(data) {
return request({
url: '/oa/city',
method: 'post',
data: data
})
}
// 修改城市
export function updateCity(data) {
return request({
url: '/oa/city',
method: 'put',
data: data
})
}
// 删除城市
export function delCity(cityIds) {
return request({
url: `/oa/city/${cityIds}`,
method: 'delete'
})
}

View File

@@ -0,0 +1,371 @@
<template>
<div class="amap-city-select">
<el-input
v-model="cityName"
:placeholder="placeholder"
@focus="openMapDialog"
clearable
readonly
>
<i slot="prefix" class="el-icon-location" style="color: #409eff"></i>
</el-input>
<el-dialog
title="选择出差城市 / 地点"
:visible.sync="dialogVisible"
width="900px"
:append-to-body="true"
@opened="onDialogOpened"
>
<div class="map-selector">
<div class="city-sidebar">
<div class="search-box">
<el-input
v-model="searchKeyword"
placeholder="输入城市或具体地点搜索"
size="small"
prefix-icon="el-icon-search"
@keyup.enter.native="searchLocation"
clearable
@clear="searchKeyword = ''"
>
<el-button slot="append" @click="searchLocation">搜索</el-button>
</el-input>
</div>
<!-- 热门城市模块 -->
<div class="hot-cities" v-if="!searchKeyword || searchResults.length === 0">
<div class="section-title">热门城市</div>
<div class="city-list">
<span
v-for="city in hotCities"
:key="city"
class="city-item"
@click="selectHotCity(city)"
>
{{ city }}
</span>
</div>
</div>
<!-- 搜索结果模块 -->
<div class="search-results" v-if="searchResults.length > 0">
<div class="section-title">搜索结果</div>
<div class="result-list">
<div
v-for="(result, index) in searchResults"
:key="index"
class="result-item"
@click="selectLocation(result)"
>
<i class="el-icon-location-outline"></i>
<div class="result-info">
<div class="poi-name">{{ result.name }}</div>
<div class="poi-address">{{ result.address }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="map-container">
<div id="amapContainer" class="amap-wrapper"></div>
<div class="map-tip">
<i class="el-icon-info"></i> 点击地图上的位置或搜索地点自动识别归属城市
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<div class="selected-info" v-if="selectedCity">
识别城市<span class="selected-city">{{ selectedCity }}</span>
<span v-if="selectedPoi" style="margin-left: 10px; color: #909399; font-size: 12px;">
( {{ selectedPoi }} )
</span>
</div>
<div v-else></div>
<div>
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmCity" :disabled="!selectedCity">确定</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import AMapLoader from '@amap/amap-jsapi-loader'
import { getAmapKey, getAmapSecurityKey } from '@/api/hrm/travel'
export default {
name: 'AmapCitySelect',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '请选择出差地点' }
},
data() {
return {
cityName: '',
dialogVisible: false,
searchKeyword: '',
searchResults: [],
selectedCity: '', // 最终表单需要的城市名
selectedPoi: '', // 具体的地点名(仅用于展示)
map: null,
geocoder: null,
placeSearch: null, // 新增地点搜索对象
marker: null,
AMap: null,
hotCities: [
'北京市', '上海市', '广州市', '深圳市',
'杭州市', '南京市', '成都市', '武汉市',
'重庆市', '天津市', '苏州市', '西安市'
]
}
},
watch: {
value: {
immediate: true,
handler(val) { this.cityName = val || '' }
}
},
methods: {
async onDialogOpened() {
const AMap = await this.loadAMap()
if (!AMap) return
this.$nextTick(() => {
this.initMap()
})
},
async loadAMap() {
if (this.AMap && window._AMapSecurityConfig) return this.AMap
try {
const [keyRes, securityRes] = await Promise.all([getAmapKey(), getAmapSecurityKey()])
const amapkey = keyRes.data || keyRes.msg
const securityKey = securityRes.data || securityRes.msg
if (!amapkey) throw new Error('未获取到高德地图 Key')
window._AMapSecurityConfig = { securityJsCode: securityKey }
this.AMap = await AMapLoader.load({
key: amapkey,
version: '2.0',
// 加入 PlaceSearch 插件
plugins: ['AMap.Geocoder', 'AMap.PlaceSearch']
})
return this.AMap
} catch (error) {
console.error('地图加载失败:', error)
this.$message.error('地图加载失败,请重试')
return null
}
},
initMap() {
const container = document.getElementById('amapContainer')
if (!container) {
setTimeout(() => this.initMap(), 100)
return
}
if (this.map) {
this.map.destroy()
this.map = null
}
this.map = new this.AMap.Map('amapContainer', {
zoom: 12,
center: [116.397428, 39.90923], // 默认北京
resizeEnable: true
})
this.geocoder = new this.AMap.Geocoder()
// 初始化地点搜索插件
this.placeSearch = new this.AMap.PlaceSearch({
pageSize: 15, // 单页显示结果条数
pageIndex: 1, // 页码
autoFitView: false // 禁用自动调整视图,我们自己控制
})
// 监听地图点击
this.map.on('click', (e) => {
const lng = e.lnglat.getLng()
const lat = e.lnglat.getLat()
this.selectedPoi = '地图选点'
this.getCityByLngLat(lng, lat, true)
})
},
// 通过经纬度逆解析城市
getCityByLngLat(lng, lat, setCenter = false) {
this.geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.regeocode) {
const addrComp = result.regeocode.addressComponent
let city = addrComp.city
// 直辖市的 city 可能是空的,取 province
if (!city || city === '[]' || city.length === 0) {
city = addrComp.province
}
this.selectedCity = city
this.addMarker(lng, lat)
if(setCenter) {
this.map.setCenter([lng, lat])
}
}
})
},
// 在地图上打点
addMarker(lng, lat) {
if (this.marker) {
this.marker.setMap(null)
}
this.marker = new this.AMap.Marker({
position: [lng, lat],
map: this.map,
animation: 'AMAP_ANIMATION_DROP' // 加上掉落动画
})
},
openMapDialog() {
this.dialogVisible = true
this.selectedCity = this.value || ''
this.selectedPoi = ''
this.searchKeyword = ''
this.searchResults = []
},
// 搜索地点核心方法
async searchLocation() {
if (!this.searchKeyword.trim()) {
this.searchResults = []
return
}
await this.loadAMap()
// 使用 PlaceSearch 搜索具体地点
this.placeSearch.search(this.searchKeyword, (status, result) => {
if (status === 'complete' && result.info === 'OK') {
// 提取返回的 POI 列表
const pois = result.poiList.pois
this.searchResults = pois.map(poi => ({
id: poi.id,
name: poi.name,
address: poi.address && typeof poi.address === 'string' ? poi.address : poi.adname,
location: poi.location, // 包含 lng/lat 的对象
city: poi.cityname || poi.pname // 城市名
}))
} else {
this.searchResults = []
this.$message.info('未找到相关地点,请尝试更换关键词')
}
})
},
// 选中左侧搜索结果列表的具体地点
selectLocation(poi) {
if (!poi.location) {
this.$message.warning('该地点缺少坐标信息')
return
}
const lng = poi.location.lng
const lat = poi.location.lat
// 设置地图中心点并放大(层级15看街道)
this.map.setZoomAndCenter(15, [lng, lat])
this.addMarker(lng, lat)
// 更新选择数据
this.selectedCity = poi.city
this.selectedPoi = poi.name
this.$message.success(`已定位到:${poi.name}`)
},
// 选中热门城市
async selectHotCity(city) {
await this.loadAMap()
this.selectedCity = city
this.selectedPoi = city
// 获取城市中心点坐标
this.geocoder.getLocation(city, (status, result) => {
if (status === 'complete' && result.geocodes.length) {
const loc = result.geocodes[0].location
this.map.setZoomAndCenter(11, [loc.lng, loc.lat])
this.addMarker(loc.lng, loc.lat)
}
})
},
confirmCity() {
if (this.selectedCity) {
this.cityName = this.selectedCity
// 组件 v-model 抛出城市名
this.$emit('input', this.selectedCity)
// 如果你需要具体的地点名,可以额外抛出一个事件
this.$emit('poi-change', { city: this.selectedCity, poi: this.selectedPoi })
this.closeDialog()
}
},
closeDialog() {
this.dialogVisible = false
}
}
}
</script>
<style lang="scss" scoped>
.amap-city-select { width: 100%; }
.map-selector { display: flex; gap: 20px; min-height: 450px;
.city-sidebar { width: 300px; flex-shrink: 0; display: flex; flex-direction: column;
.search-box { margin-bottom: 16px; }
.section-title { font-size: 14px; font-weight: bold; color: #303133; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
.hot-cities .city-list { display: flex; flex-wrap: wrap; gap: 10px;
.city-item { padding: 6px 14px; background: #f4f4f5; color: #606266; border-radius: 4px; font-size: 13px; cursor: pointer; transition: all 0.2s;
&:hover { background: #409eff; color: white; }
}
}
/* 搜索结果列表样式升级 */
.search-results {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.result-list {
flex: 1; overflow-y: auto; padding-right: 5px;
/* 自定义滚动条 */
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 4px; }
.result-item {
padding: 12px 10px; cursor: pointer; display: flex; align-items: flex-start; gap: 10px; border-bottom: 1px solid #ebeef5; transition: background 0.2s;
&:hover { background: #f0f7ff; }
i { color: #409eff; margin-top: 3px; font-size: 16px; }
.result-info {
flex: 1; overflow: hidden;
.poi-name { font-size: 14px; color: #303133; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.poi-address { font-size: 12px; color: #909399; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
}
}
}
.map-container { flex: 1; display: flex; flex-direction: column;
.amap-wrapper { flex: 1; width: 100%; border: 1px solid #dcdfe6; border-radius: 6px; overflow: hidden; }
.map-tip { margin-top: 10px; font-size: 12px; color: #909399; text-align: center; i { margin-right: 4px; color: #e6a23c; } }
}
}
.dialog-footer { display: flex; justify-content: space-between; align-items: center;
.selected-info { font-size: 14px; color: #606266; .selected-city { color: #409eff; font-weight: bold; font-size: 15px; } }
}
</style>

Some files were not shown because too many files have changed in this diff Show More