feat: OCR 服务 + 会议材料模块
ry-ocr/ (新)
本地发票识别微服务 (PaddleOCR 3.x + FastAPI, 8801)
- QR 优先: 扫到二维码即取开票时间/发票号/金额; 没扫到/格式不合法直接判非发票, 不跑 OCR
- 配置 QR_FULL_OCR 控制快路径(false, 0.2s)还是全字段(true, 4.5s)
- /recognize/invoice (multipart) + /recognize/invoice/by-path (本地路径, 白名单) + /recognize/text
- is_invoice / from_qr / qr_raw / qr_error / error_code 字段
- 12 字段发票抽取 (regex + 启发式, 左右主体识别)
- 超时保护 (15s 单页 / 60s 总流程) + PaddleOCR 单例 + ThreadPoolExecutor
ry-api/ruoyi-business/
- pom.xml: 加 hutool-http/json/core 5.8.27, lombok 1.18.30 (OcrClient @Slf4j 所需)
- ocr/: OcrClient + InvoiceResult/Fields/Line + ZipExtractor + InvoiceOcrScheduler
- oss/: OssUploader + OssConfMeta (OCR 识别后重传 OSS)
- config/: OcrConfig + OcrExecutorConfig (后台线程池)
- service/impl/InvoiceOcrService: 后台提交 OCR, ZIP 路径解压识别, 替换场景先清旧
- 会议材料 CRUD 全套 (BizMeetingAuditLog/Executor/Invoice/Material/Supervisor):
controller + service + mapper + domain + xml
ry-vue3/
- MeetingDetail.vue (新建): 会议详情页 (含评分维度章节, 改只读)
- Meetings.vue / OssFileUploader.vue / router / Login.vue: 适配新字段
ry-api/ruoyi-admin/
- RuoYiApplication.java + application.yml: 启用 @Async 异步支持
_self/
- manager_meetings.md / manager_meeting_detail.md: 文档
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.business.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import com.ruoyi.business.ocr.OcrClient;
|
||||
|
||||
/**
|
||||
* ry-ocr 微服务集成配置
|
||||
* <p>
|
||||
* yml 配置: ruoyi.ocr.base-url (默认 http://127.0.0.1:8801)
|
||||
*/
|
||||
@Configuration
|
||||
public class OcrConfig {
|
||||
|
||||
@Value("${ruoyi.ocr.base-url:http://127.0.0.1:8801}")
|
||||
private String ocrBaseUrl;
|
||||
|
||||
@Bean
|
||||
public OcrClient ocrClient() {
|
||||
return new OcrClient(ocrBaseUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.business.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* OCR 后台执行器配置
|
||||
* <p>
|
||||
* 16 个固定线程, 用于:
|
||||
* - 单文件上传后, 后台异步 OCR (前端立即返回 SUBMITTED)
|
||||
* - ZIP 解压后, 后台逐张识别 + 重传 OSS
|
||||
* - 兜底调度 InvoiceOcrScheduler 重试 UNRECOGNIZED 超过 5 分钟的记录
|
||||
*/
|
||||
@Configuration
|
||||
public class OcrExecutorConfig
|
||||
{
|
||||
/**
|
||||
* 16 线程 FixedThreadPool
|
||||
* <p>
|
||||
* 线程数选 16: 与阿里云 OSS 默认下载并发限速对齐, 兼顾单台机器 ry-ocr 服务能力
|
||||
* (单张发票 OCR 平均 1-3s, 16 线程 ≈ 5-15 张/秒)
|
||||
*/
|
||||
@Bean(name = "ocrExecutor", destroyMethod = "shutdown")
|
||||
public ExecutorService ocrExecutor()
|
||||
{
|
||||
return Executors.newFixedThreadPool(16);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.business.domain.BizMeetingAuditLog;
|
||||
import com.ruoyi.business.service.IBizMeetingAuditLogService;
|
||||
|
||||
/**
|
||||
* 会议审核流程日志 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meetingAuditLog")
|
||||
public class BizMeetingAuditLogController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingAuditLogService bizMeetingAuditLogService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(BizMeetingAuditLog bizMeetingAuditLog) {
|
||||
startPage();
|
||||
List<BizMeetingAuditLog> list = bizMeetingAuditLogService.selectList(bizMeetingAuditLog);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public AjaxResult getInfo(@PathVariable("id") Long id) {
|
||||
return success(bizMeetingAuditLogService.getById(id));
|
||||
}
|
||||
|
||||
@Log(title = "会议审核日志", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody BizMeetingAuditLog bizMeetingAuditLog) {
|
||||
return toAjax(bizMeetingAuditLogService.insert(bizMeetingAuditLog));
|
||||
}
|
||||
|
||||
@Log(title = "会议审核日志", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody BizMeetingAuditLog bizMeetingAuditLog) {
|
||||
return toAjax(bizMeetingAuditLogService.updateByPrimaryKey(bizMeetingAuditLog));
|
||||
}
|
||||
|
||||
@Log(title = "会议审核日志", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||
return toAjax(bizMeetingAuditLogService.deleteByPrimaryKeys(ids));
|
||||
}
|
||||
}
|
||||
+221
-19
@@ -1,8 +1,15 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.domain.BizMeetingAuditLog;
|
||||
import com.ruoyi.business.domain.BizMeetingSupervisor;
|
||||
import com.ruoyi.business.domain.BizMeetingExecutor;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
import com.ruoyi.business.service.IBizMeetingAuditLogService;
|
||||
import com.ruoyi.business.service.IBizMeetingSupervisorService;
|
||||
import com.ruoyi.business.service.IBizMeetingExecutorService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
@@ -10,22 +17,34 @@ import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
|
||||
/**
|
||||
* 会议Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meeting")
|
||||
public class BizMeetingController extends BaseController
|
||||
{
|
||||
public class BizMeetingController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingService bizMeetingService;
|
||||
@Autowired
|
||||
private IBizMeetingAttendeeService attendeeService;
|
||||
@Autowired
|
||||
private IBizMeetingMaterialService bizMeetingMaterialService;
|
||||
@Autowired
|
||||
private IBizMeetingAuditLogService bizMeetingAuditLogService;
|
||||
@Autowired
|
||||
private IBizMeetingSupervisorService bizMeetingSupervisorService;
|
||||
@Autowired
|
||||
private IBizMeetingExecutorService bizMeetingExecutorService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(BizMeeting bizMeeting)
|
||||
{
|
||||
public TableDataInfo list(BizMeeting bizMeeting) {
|
||||
// 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if ("doctor".equals(roleType) || "expert".equals(roleType)) {
|
||||
@@ -35,42 +54,225 @@ public class BizMeetingController extends BaseController
|
||||
List<BizMeeting> list = bizMeetingService.selectList(bizMeeting);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@GetMapping("/{meetingId}")
|
||||
public AjaxResult getInfo(@PathVariable("meetingId") Long meetingId)
|
||||
{
|
||||
public AjaxResult getInfo(@PathVariable("meetingId") Long meetingId) {
|
||||
return success(bizMeetingService.getById(meetingId));
|
||||
}
|
||||
@Autowired
|
||||
private IBizMeetingAttendeeService attendeeService;
|
||||
|
||||
@Log(title = "会议", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody BizMeeting bizMeeting)
|
||||
{
|
||||
public AjaxResult add(@RequestBody BizMeeting bizMeeting) {
|
||||
int rows = bizMeetingService.insert(bizMeeting);
|
||||
// 同步创建参会人中间表 (可选: 前端传 attendeeUserIds)
|
||||
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
|
||||
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
|
||||
attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds);
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
@Log(title = "会议", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody BizMeeting bizMeeting)
|
||||
{
|
||||
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
|
||||
int rows = bizMeetingService.updateByPrimaryKey(bizMeeting);
|
||||
// 同步追加参会人 (不去重, 由前端控制)
|
||||
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
|
||||
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
|
||||
attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds);
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
@Log(title = "会议", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids)
|
||||
{
|
||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||
return toAjax(bizMeetingService.deleteByPrimaryKeys(ids));
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 审核流程端点 (5 个)
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* 执行人员提交材料
|
||||
* <ul>
|
||||
* <li>校验 1: 当前用户是该会议执行人员 (强校验)</li>
|
||||
* <li>校验 2: material_audit_stage = INIT</li>
|
||||
* <li>校验 3: biz_meeting_material 至少 1 条 L_* + 至少 1 条 M_*</li>
|
||||
* </ul>
|
||||
* 通过后 material_audit_stage INIT → SUBMITTED, 记 audit_log.
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/submit-material")
|
||||
public AjaxResult submitMaterial(@PathVariable("meetingId") Long meetingId) {
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
boolean isExec = bizMeetingExecutorService.selectByMeetingId(meetingId).stream()
|
||||
.anyMatch(e -> userId.equals(e.getUserId()));
|
||||
if (!isExec) throw new ServiceException("您不是该会议执行人员, 无法提交材料");
|
||||
|
||||
if (!"INIT".equals(m.getMaterialAuditStage())) {
|
||||
throw new ServiceException("当前阶段 (" + m.getMaterialAuditStage() + ") 不允许提交材料");
|
||||
}
|
||||
|
||||
List<BizMeetingMaterial> mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
||||
boolean hasLabor = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("L_"));
|
||||
boolean hasService = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("M_"));
|
||||
if (!hasLabor || !hasService) {
|
||||
throw new ServiceException("请同时上传劳务材料和会务材料");
|
||||
}
|
||||
|
||||
m.setMaterialAuditStage("SUBMITTED");
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(meetingId, "MATERIAL", "SUBMITTED", "APPROVED", "执行人员提交材料");
|
||||
return success("SUBMITTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行人员提交凭证 (校验 LV_PAYMENT + SV_PAYMENT)
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/submit-voucher")
|
||||
public AjaxResult submitVoucher(@PathVariable("meetingId") Long meetingId) {
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
boolean isExec = bizMeetingExecutorService.selectByMeetingId(meetingId).stream()
|
||||
.anyMatch(e -> userId.equals(e.getUserId()));
|
||||
if (!isExec) throw new ServiceException("您不是该会议执行人员, 无法提交凭证");
|
||||
|
||||
if (!"INIT".equals(m.getVoucherAuditStage())) {
|
||||
throw new ServiceException("当前阶段 (" + m.getVoucherAuditStage() + ") 不允许提交凭证");
|
||||
}
|
||||
|
||||
List<BizMeetingMaterial> mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
||||
boolean hasLv = mats.stream().anyMatch(x -> "LV_PAYMENT".equals(x.getSubType()));
|
||||
boolean hasSv = mats.stream().anyMatch(x -> "SV_PAYMENT".equals(x.getSubType()));
|
||||
if (!hasLv || !hasSv) {
|
||||
throw new ServiceException("请同时上传劳务凭证和会务凭证");
|
||||
}
|
||||
|
||||
m.setVoucherAuditStage("SUBMITTED");
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(meetingId, "VOUCHER", "SUBMITTED", "APPROVED", "执行人员提交凭证");
|
||||
return success("SUBMITTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规审核 (role_type=manager)
|
||||
* body: { "auditType": "MATERIAL"|"VOUCHER", "approved": true|false, "opinion": "..." }
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/audit-compliance")
|
||||
public AjaxResult auditCompliance(@PathVariable("meetingId") Long meetingId, @RequestBody AuditBody body) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"manager".equals(roleType)) throw new ServiceException("仅合规人员可操作");
|
||||
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
String auditType = body.getAuditType();
|
||||
if (!"MATERIAL".equals(auditType) && !"VOUCHER".equals(auditType)) {
|
||||
throw new ServiceException("auditType 必须是 MATERIAL 或 VOUCHER");
|
||||
}
|
||||
String currentStage = "MATERIAL".equals(auditType) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
|
||||
if (!"SUBMITTED".equals(currentStage)) {
|
||||
throw new ServiceException("当前阶段 (" + currentStage + ") 不允许合规审核");
|
||||
}
|
||||
if (Boolean.FALSE.equals(body.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||
throw new ServiceException("拒绝时意见不能为空");
|
||||
}
|
||||
|
||||
String result = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "REJECTED";
|
||||
String newStage = Boolean.TRUE.equals(body.getApproved()) ? "COMPLIANCE_APPROVED" : "SUBMITTED";
|
||||
if ("MATERIAL".equals(auditType)) {
|
||||
m.setMaterialAuditStage(newStage);
|
||||
} else {
|
||||
m.setVoucherAuditStage(newStage);
|
||||
}
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(meetingId, auditType, newStage, result, body.getOpinion());
|
||||
return success(newStage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监察审核 (强校验: 当前用户必须是该会议监察员)
|
||||
* body: { "auditType": "MATERIAL"|"VOUCHER", "approved": true|false, "opinion": "..." }
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/audit-supervision")
|
||||
public AjaxResult auditSupervision(@PathVariable("meetingId") Long meetingId, @RequestBody AuditBody body) {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
|
||||
.anyMatch(s -> userId.equals(s.getUserId()));
|
||||
if (!isSupervisor) throw new ServiceException("您不是该会议监察员, 无权监察");
|
||||
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
String auditType = body.getAuditType();
|
||||
if (!"MATERIAL".equals(auditType) && !"VOUCHER".equals(auditType)) {
|
||||
throw new ServiceException("auditType 必须是 MATERIAL 或 VOUCHER");
|
||||
}
|
||||
String currentStage = "MATERIAL".equals(auditType) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
|
||||
if (!"COMPLIANCE_APPROVED".equals(currentStage)) {
|
||||
throw new ServiceException("当前阶段 (" + currentStage + ") 不允许监察");
|
||||
}
|
||||
if (Boolean.FALSE.equals(body.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||
throw new ServiceException("拒绝时意见不能为空");
|
||||
}
|
||||
|
||||
String result = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "REJECTED";
|
||||
String newStage = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "SUBMITTED";
|
||||
if ("MATERIAL".equals(auditType)) {
|
||||
m.setMaterialAuditStage(newStage);
|
||||
} else {
|
||||
m.setVoucherAuditStage(newStage);
|
||||
}
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(meetingId, auditType, newStage, result, body.getOpinion());
|
||||
return success(newStage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核轨迹 (audit_log 列表, 按时间排序)
|
||||
*/
|
||||
@GetMapping("/{meetingId}/audit-trail")
|
||||
public AjaxResult auditTrail(@PathVariable("meetingId") Long meetingId) {
|
||||
BizMeetingAuditLog q = new BizMeetingAuditLog();
|
||||
q.setMeetingId(meetingId);
|
||||
List<BizMeetingAuditLog> list = bizMeetingAuditLogService.selectList(q);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部: 写一条 audit_log
|
||||
*/
|
||||
private void appendAuditLog(Long meetingId, String auditType, String stage, String result, String opinion) {
|
||||
BizMeetingAuditLog log = new BizMeetingAuditLog();
|
||||
log.setMeetingId(meetingId);
|
||||
log.setAuditor(SecurityUtils.getUsername());
|
||||
log.setAuditType(auditType);
|
||||
log.setCurrentStage(stage);
|
||||
log.setAuditResult(result);
|
||||
log.setOpinion(opinion);
|
||||
log.setCreateTime(new Date());
|
||||
log.setAuditTime(new Date());
|
||||
bizMeetingAuditLogService.insert(log);
|
||||
}
|
||||
|
||||
/** request body for audit endpoints */
|
||||
public static class AuditBody {
|
||||
private String auditType; // MATERIAL / VOUCHER
|
||||
private Boolean approved; // true=通过 false=拒绝
|
||||
private String opinion; // 意见
|
||||
public String getAuditType() { return auditType; }
|
||||
public void setAuditType(String auditType) { this.auditType = auditType; }
|
||||
public Boolean getApproved() { return approved; }
|
||||
public void setApproved(Boolean approved) { this.approved = approved; }
|
||||
public String getOpinion() { return opinion; }
|
||||
public void setOpinion(String opinion) { this.opinion = opinion; }
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeetingExecutor;
|
||||
import com.ruoyi.business.service.IBizMeetingExecutorService;
|
||||
|
||||
/**
|
||||
* 会议-执行人员 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meeting/executor")
|
||||
public class BizMeetingExecutorController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingExecutorService bizMeetingExecutorService;
|
||||
|
||||
/** 查该会议的所有执行人员 */
|
||||
@GetMapping("/list/{meetingId}")
|
||||
public AjaxResult list(@PathVariable("meetingId") Long meetingId) {
|
||||
return success(bizMeetingExecutorService.selectByMeetingId(meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配执行人员 (全删全插)
|
||||
* body: { "userIds": [1, 2, 3] }
|
||||
*/
|
||||
@Log(title = "会议-执行人员", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{meetingId}")
|
||||
public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) {
|
||||
Long assignedBy = SecurityUtils.getUserId();
|
||||
int n = bizMeetingExecutorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy);
|
||||
return success(n);
|
||||
}
|
||||
|
||||
/** request body wrapper */
|
||||
public static class AssignBody {
|
||||
private List<Long> userIds;
|
||||
public List<Long> getUserIds() { return userIds; }
|
||||
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.business.service.impl.InvoiceOcrService;
|
||||
|
||||
/**
|
||||
* 会议发票识别 Controller (v3)
|
||||
* <p>
|
||||
* v3 变更:
|
||||
* - 端点改为"提交即返回 SUBMITTED", OCR 在后台 ExecutorService 跑
|
||||
* - 新增 isZip 字段: true → zip 路径(解压 → 重传 OSS → 写多行 invoice)
|
||||
* - 新增 oldMaterialId 字段: 替换场景, 后端先清旧 invoice + material.amount=0
|
||||
* - 不再走旧的同步 recognizeAndSave, 已被 submitRecognition 取代
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meeting/invoice")
|
||||
public class BizMeetingInvoiceController extends BaseController
|
||||
{
|
||||
|
||||
@Autowired
|
||||
private InvoiceOcrService invoiceOcrService;
|
||||
|
||||
/**
|
||||
* 提交发票识别 (后台执行, 立即返回)
|
||||
* <p>
|
||||
* body: {
|
||||
* "materialId": 123,
|
||||
* "meetingId": 456,
|
||||
* "ossUrl": "https://...",
|
||||
* "isZip": false, // 新增
|
||||
* "oldMaterialId": null // 新增, 替换时携带原 materialId
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/recognize")
|
||||
public AjaxResult recognize(@RequestBody RecognizeBody body)
|
||||
{
|
||||
InvoiceOcrService.RecognizeResult r = invoiceOcrService.submitRecognition(
|
||||
body.getMaterialId(),
|
||||
body.getMeetingId(),
|
||||
body.getOssUrl(),
|
||||
body.isZip(),
|
||||
body.getOldMaterialId());
|
||||
return success(r);
|
||||
}
|
||||
|
||||
/** request body wrapper */
|
||||
public static class RecognizeBody
|
||||
{
|
||||
private Long materialId;
|
||||
private Long meetingId;
|
||||
private String ossUrl;
|
||||
/** v3 新增: true=zip 路径(解压遍历), false=单文件路径 */
|
||||
private boolean isZip;
|
||||
/** v3 新增: 替换场景携带的旧 materialId, 后端先 DELETE invoice WHERE material_id=old + material.amount=0 */
|
||||
private Long oldMaterialId;
|
||||
|
||||
public Long getMaterialId() { return materialId; }
|
||||
public void setMaterialId(Long materialId) { this.materialId = materialId; }
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
public String getOssUrl() { return ossUrl; }
|
||||
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
|
||||
public boolean isZip() { return isZip; }
|
||||
public void setZip(boolean zip) { isZip = zip; }
|
||||
public Long getOldMaterialId() { return oldMaterialId; }
|
||||
public void setOldMaterialId(Long oldMaterialId) { this.oldMaterialId = oldMaterialId; }
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
|
||||
/**
|
||||
* 会议材料 Controller
|
||||
* <p>
|
||||
* 单表设计: GET 查 / PUT 全删全插 (替代 4 张分表 + 4 套端点)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meetingMaterial")
|
||||
public class BizMeetingMaterialController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingMaterialService bizMeetingMaterialService;
|
||||
|
||||
/**
|
||||
* 查该会议的所有材料记录
|
||||
*/
|
||||
@GetMapping("/{meetingId}")
|
||||
public AjaxResult list(@PathVariable("meetingId") Long meetingId) {
|
||||
return success(bizMeetingMaterialService.selectByMeetingId(meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 (全删全插)
|
||||
* <p>
|
||||
* body 是该会议当前的所有材料记录. 前端按 rows 过滤 url 非空后整体 PUT.
|
||||
* creator_id 后端兜底, 防止前端伪造.
|
||||
* 返回值是插入后带 id 的 list (前端用于触发 OCR 识别).
|
||||
*/
|
||||
@Log(title = "会议材料", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{meetingId}")
|
||||
public AjaxResult save(@PathVariable("meetingId") Long meetingId, @RequestBody List<BizMeetingMaterial> list) {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
if (list != null) {
|
||||
for (BizMeetingMaterial m : list) {
|
||||
if (m.getCreatorId() == null) {
|
||||
m.setCreatorId(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<BizMeetingMaterial> saved = bizMeetingMaterialService.replaceByMeetingId(meetingId, list);
|
||||
return success(saved);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeetingSupervisor;
|
||||
import com.ruoyi.business.service.IBizMeetingSupervisorService;
|
||||
|
||||
/**
|
||||
* 会议-监察员 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meeting/supervisor")
|
||||
public class BizMeetingSupervisorController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingSupervisorService bizMeetingSupervisorService;
|
||||
|
||||
/** 查该会议的所有监察员 */
|
||||
@GetMapping("/list/{meetingId}")
|
||||
public AjaxResult list(@PathVariable("meetingId") Long meetingId) {
|
||||
return success(bizMeetingSupervisorService.selectByMeetingId(meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配监察员 (全删全插)
|
||||
* body: { "userIds": [1, 2, 3] }
|
||||
*/
|
||||
@Log(title = "会议-监察员", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{meetingId}")
|
||||
public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) {
|
||||
Long assignedBy = SecurityUtils.getUserId();
|
||||
int n = bizMeetingSupervisorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy);
|
||||
return success(n);
|
||||
}
|
||||
|
||||
/** request body wrapper */
|
||||
public static class AssignBody {
|
||||
private List<Long> userIds;
|
||||
public List<Long> getUserIds() { return userIds; }
|
||||
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -220,7 +220,7 @@ public class BizProjectController extends BaseController
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
int cnt = 0;
|
||||
for (BizProjectRating r : all) {
|
||||
int s = safeLong(r.getQualityScore()) + safeLong(r.getResponseScore())
|
||||
long s = safeLong(r.getQualityScore()) + safeLong(r.getResponseScore())
|
||||
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
|
||||
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ public class BizMeeting extends BaseEntity {
|
||||
/** 监察时间 (与 DB datetime 对齐) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date supervisionTime;
|
||||
/** 材料审核阶段 (INIT=待提交, 后续阶段开发中定) */
|
||||
private String materialAuditStage;
|
||||
/** 凭证审核阶段 (INIT=待提交, 后续阶段开发中定) */
|
||||
private String voucherAuditStage;
|
||||
/** 邀请函URL */
|
||||
private String invitationUrl;
|
||||
/** 日程海报URL */
|
||||
@@ -129,6 +133,10 @@ public class BizMeeting extends BaseEntity {
|
||||
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
|
||||
public String getLaborSigned() { return laborSigned; }
|
||||
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
||||
public String getMaterialAuditStage() { return materialAuditStage; }
|
||||
public void setMaterialAuditStage(String materialAuditStage) { this.materialAuditStage = materialAuditStage; }
|
||||
public String getVoucherAuditStage() { return voucherAuditStage; }
|
||||
public void setVoucherAuditStage(String voucherAuditStage) { this.voucherAuditStage = voucherAuditStage; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long[] getAttendeeUserIds() { return attendeeUserIds; }
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 会议审核流程日志对象 biz_meeting_audit_log
|
||||
* <p>
|
||||
* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段.
|
||||
* 与 biz_meeting.material_audit_stage / voucher_audit_stage 配合, 还原审核轨迹.
|
||||
*/
|
||||
public class BizMeetingAuditLog {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 记录ID */
|
||||
private Long id;
|
||||
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** 审核人 (username 或人工填入) */
|
||||
private String auditor;
|
||||
|
||||
/** 审核意见 */
|
||||
private String opinion;
|
||||
|
||||
/** 当前阶段 (INIT / ... 后续开发中定) */
|
||||
private String currentStage;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
/** 审核时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date auditTime;
|
||||
|
||||
/** 审核类型 (MATERIAL / VOUCHER) */
|
||||
private String auditType;
|
||||
|
||||
/** 审核结果 (APPROVED / REJECTED) */
|
||||
private String auditResult;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public String getAuditor() { return auditor; }
|
||||
public void setAuditor(String auditor) { this.auditor = auditor; }
|
||||
|
||||
public String getOpinion() { return opinion; }
|
||||
public void setOpinion(String opinion) { this.opinion = opinion; }
|
||||
|
||||
public String getCurrentStage() { return currentStage; }
|
||||
public void setCurrentStage(String currentStage) { this.currentStage = currentStage; }
|
||||
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
|
||||
public Date getAuditTime() { return auditTime; }
|
||||
public void setAuditTime(Date auditTime) { this.auditTime = auditTime; }
|
||||
|
||||
public String getAuditType() { return auditType; }
|
||||
public void setAuditType(String auditType) { this.auditType = auditType; }
|
||||
|
||||
public String getAuditResult() { return auditResult; }
|
||||
public void setAuditResult(String auditResult) { this.auditResult = auditResult; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 会议-执行人员 关联对象 biz_meeting_executor (1:N)
|
||||
* <p>
|
||||
* 当前阶段: 仅取 executor 主账号 (parent_user_id IS NULL), 后续表结构预留支持子账号.
|
||||
*/
|
||||
public class BizMeetingExecutor {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 记录ID */
|
||||
private Long id;
|
||||
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** sys_user.user_id (executor 主账号, 后续支持子账号) */
|
||||
private Long userId;
|
||||
|
||||
/** 分配人 user_id (审计) */
|
||||
private Long assignedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public Long getAssignedBy() { return assignedBy; }
|
||||
public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; }
|
||||
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 会议发票识别对象 biz_meeting_invoice
|
||||
* <p>
|
||||
* 一张会议材料 (biz_meeting_material.id) 对应一条发票记录.
|
||||
* 仅当 OCR 识别为发票时插入, 不是发票则不创建任何行.
|
||||
* 金额 amount 同时回写到 biz_meeting_material.amount.
|
||||
*/
|
||||
public class BizMeetingInvoice {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 记录ID */
|
||||
private Long id;
|
||||
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** 关联材料ID (FK -> biz_meeting_material.id) */
|
||||
private Long materialId;
|
||||
|
||||
/** OSS URL */
|
||||
private String ossUrl;
|
||||
|
||||
/** 发票类型 (MAIN=主发票, SUB=子发票) */
|
||||
private String invoiceType;
|
||||
|
||||
/** 价税合计 (OCR 识别金额, 同时回写到 material) */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 提交人 user_id */
|
||||
private Long creatorId;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 识别状态:
|
||||
* UNRECOGNIZED=已插入待 OCR (单文件路径)
|
||||
* RECOGNIZED=已识别完成 (默认, ZIP 路径直接走识别完成)
|
||||
* FAILED=识别失败
|
||||
*/
|
||||
private String recognizeStatus;
|
||||
|
||||
/** 识别失败原因 */
|
||||
private String errorMsg;
|
||||
|
||||
/** 原始文件名 (zip 解压时记录, 单文件路径可空) */
|
||||
private String sourceFilename;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public Long getMaterialId() { return materialId; }
|
||||
public void setMaterialId(Long materialId) { this.materialId = materialId; }
|
||||
|
||||
public String getOssUrl() { return ossUrl; }
|
||||
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
|
||||
|
||||
public String getInvoiceType() { return invoiceType; }
|
||||
public void setInvoiceType(String invoiceType) { this.invoiceType = invoiceType; }
|
||||
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
|
||||
public Long getCreatorId() { return creatorId; }
|
||||
public void setCreatorId(Long creatorId) { this.creatorId = creatorId; }
|
||||
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
|
||||
public Date getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public String getRecognizeStatus() { return recognizeStatus; }
|
||||
public void setRecognizeStatus(String recognizeStatus) { this.recognizeStatus = recognizeStatus; }
|
||||
|
||||
public String getErrorMsg() { return errorMsg; }
|
||||
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
|
||||
|
||||
public String getSourceFilename() { return sourceFilename; }
|
||||
public void setSourceFilename(String sourceFilename) { this.sourceFilename = sourceFilename; }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 会议材料对象 biz_meeting_material (单表)
|
||||
* <p>
|
||||
* 包含 4 大类 13 子类:
|
||||
* <ul>
|
||||
* <li>material_type: SERVICE=会务材料, LABOR=劳务材料, SERVICE_VOUCHER=会务凭证, LABOR_VOUCHER=劳务凭证</li>
|
||||
* <li>sub_type: M_MATERIAL / M_HOTEL / M_TRAFFIC_BIG / M_TRAFFIC_SMALL / M_EXECUTION / M_DESIGN / M_OTHER / M_SETTLEMENT / M_INVOICE / L_DETAIL / L_AGREEMENT / SV_PAYMENT / LV_PAYMENT</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 注意: 不继承 BaseEntity — 不要 create_by / update_by / update_time 字段.
|
||||
* 提交人用 creator_id (user_id), 创建时间 create_time.
|
||||
*/
|
||||
public class BizMeetingMaterial {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 记录ID */
|
||||
private Long id;
|
||||
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** 资料类型 (4 种): SERVICE / LABOR / SERVICE_VOUCHER / LABOR_VOUCHER */
|
||||
private String materialType;
|
||||
|
||||
/** 子分类 (13 种): M_MATERIAL / M_HOTEL / ... / SV_PAYMENT / LV_PAYMENT */
|
||||
private String subType;
|
||||
|
||||
/** 文件名称 */
|
||||
private String fileName;
|
||||
|
||||
/** OSS URL */
|
||||
private String ossUrl;
|
||||
|
||||
/** 金额 (发票专用, 其他类型 = 0) */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 提交人 user_id (后端从 SecurityUtils 自动取) */
|
||||
private Long creatorId;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public String getMaterialType() { return materialType; }
|
||||
public void setMaterialType(String materialType) { this.materialType = materialType; }
|
||||
|
||||
public String getSubType() { return subType; }
|
||||
public void setSubType(String subType) { this.subType = subType; }
|
||||
|
||||
public String getFileName() { return fileName; }
|
||||
public void setFileName(String fileName) { this.fileName = fileName; }
|
||||
|
||||
public String getOssUrl() { return ossUrl; }
|
||||
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
|
||||
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
|
||||
public Long getCreatorId() { return creatorId; }
|
||||
public void setCreatorId(Long creatorId) { this.creatorId = creatorId; }
|
||||
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 会议-监察员 关联对象 biz_meeting_supervisor (1:N)
|
||||
* <p>
|
||||
* 当前阶段: 仅取 sponsor 主账号 (parent_user_id IS NULL), 后续表结构预留支持子账号.
|
||||
*/
|
||||
public class BizMeetingSupervisor {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 记录ID */
|
||||
private Long id;
|
||||
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** sys_user.user_id (sponsor 主账号, 后续支持子账号) */
|
||||
private Long userId;
|
||||
|
||||
/** 分配人 user_id (审计) */
|
||||
private Long assignedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public Long getAssignedBy() { return assignedBy; }
|
||||
public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; }
|
||||
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingAuditLog;
|
||||
|
||||
/**
|
||||
* 会议审核流程日志 Mapper 接口
|
||||
*/
|
||||
public interface BizMeetingAuditLogMapper {
|
||||
|
||||
/** 按主键查 */
|
||||
BizMeetingAuditLog selectByPrimaryKey(Long id);
|
||||
|
||||
/** 条件查询 (meetingId 可选过滤) */
|
||||
List<BizMeetingAuditLog> selectList(BizMeetingAuditLog entity);
|
||||
|
||||
/** 插入 (id 走 AUTO_INCREMENT) */
|
||||
int insert(BizMeetingAuditLog entity);
|
||||
|
||||
/** 按主键更新 */
|
||||
int updateByPrimaryKey(BizMeetingAuditLog entity);
|
||||
|
||||
/** 按主键删除单条 */
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
/** 按主键批量删除 */
|
||||
int deleteByPrimaryKeys(Long[] ids);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingExecutor;
|
||||
|
||||
/**
|
||||
* 会议-执行人员 Mapper 接口
|
||||
*/
|
||||
public interface BizMeetingExecutorMapper {
|
||||
|
||||
/** 按主键查 */
|
||||
BizMeetingExecutor selectByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID查该会议的所有执行人员 */
|
||||
List<BizMeetingExecutor> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 按 userId 查该执行人员被分配到哪些会议 */
|
||||
List<BizMeetingExecutor> selectByUserId(Long userId);
|
||||
|
||||
/** 条件查询 */
|
||||
List<BizMeetingExecutor> selectList(BizMeetingExecutor entity);
|
||||
|
||||
/** 插入 (id AUTO_INCREMENT) */
|
||||
int insert(BizMeetingExecutor entity);
|
||||
|
||||
/** 按主键更新 */
|
||||
int updateByPrimaryKey(BizMeetingExecutor entity);
|
||||
|
||||
/** 按主键删除 */
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID全删 (分配时全删全插) */
|
||||
int deleteByMeetingId(Long meetingId);
|
||||
|
||||
/** 批量插入 */
|
||||
int insertBatch(List<BizMeetingExecutor> list);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingInvoice;
|
||||
|
||||
/**
|
||||
* 会议发票 Mapper (单表 biz_meeting_invoice)
|
||||
*/
|
||||
public interface BizMeetingInvoiceMapper {
|
||||
|
||||
BizMeetingInvoice selectByPrimaryKey(Long id);
|
||||
|
||||
List<BizMeetingInvoice> selectList(BizMeetingInvoice query);
|
||||
|
||||
/** 按 material_id 查 (UK 唯一) */
|
||||
BizMeetingInvoice selectByMaterialId(Long materialId);
|
||||
|
||||
/** 按 meeting_id 查 (用于展示某个会议下所有发票) */
|
||||
List<BizMeetingInvoice> selectByMeetingId(Long meetingId);
|
||||
|
||||
int insert(BizMeetingInvoice record);
|
||||
|
||||
int updateByPrimaryKey(BizMeetingInvoice record);
|
||||
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
int deleteByPrimaryKeys(Long[] ids);
|
||||
|
||||
/** 按 material_id 删除 (用于重传时清掉旧记录) */
|
||||
int deleteByMaterialId(Long materialId);
|
||||
|
||||
/**
|
||||
* 兜底扫描: 查询 UNRECOGNIZED 状态且 create_time 早于 N 分钟前的行
|
||||
* <p>
|
||||
* 用于 InvoiceOcrScheduler 每 60s 扫一次, 处理程序重启/OCR 服务临时挂掉导致的遗漏
|
||||
*
|
||||
* @param minutes 分钟阈值
|
||||
*/
|
||||
List<BizMeetingInvoice> selectUnrecognizedOlderThanMinutes(int minutes);
|
||||
|
||||
/**
|
||||
* 单文件后台 OCR 完成: 按 material_id 更新状态 (前提是 UNRECOGNIZED → RECOGNIZED/FAILED)
|
||||
*/
|
||||
int updateStatusByMaterial(@org.apache.ibatis.annotations.Param("materialId") Long materialId,
|
||||
@org.apache.ibatis.annotations.Param("recognizeStatus") String recognizeStatus,
|
||||
@org.apache.ibatis.annotations.Param("errorMsg") String errorMsg);
|
||||
|
||||
/**
|
||||
* 单文件后台 OCR 完成: 按 material_id 更新金额 (与状态更新分开调用)
|
||||
*/
|
||||
int updateAmountByMaterial(@org.apache.ibatis.annotations.Param("materialId") Long materialId,
|
||||
@org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 兜底 OCR: 按主键更新 (状态 + 金额 + 错误信息)
|
||||
*/
|
||||
int updateStatusAndAmountByPrimaryKey(@org.apache.ibatis.annotations.Param("id") Long id,
|
||||
@org.apache.ibatis.annotations.Param("recognizeStatus") String recognizeStatus,
|
||||
@org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount,
|
||||
@org.apache.ibatis.annotations.Param("errorMsg") String errorMsg);
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
|
||||
/**
|
||||
* 会议材料 Mapper 接口 (单表 biz_meeting_material)
|
||||
*/
|
||||
public interface BizMeetingMaterialMapper {
|
||||
|
||||
/** 按主键查 */
|
||||
BizMeetingMaterial selectByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID查该会议所有材料记录 */
|
||||
List<BizMeetingMaterial> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 插入 (id 走 DB AUTO_INCREMENT, 不接受前端传入的 id) */
|
||||
int insert(BizMeetingMaterial entity);
|
||||
|
||||
/** 按主键更新 (一般不用, 全删全插代替) */
|
||||
int updateByPrimaryKey(BizMeetingMaterial entity);
|
||||
|
||||
/** 按主键删除单条 */
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID删除该会议所有材料记录 (save 时先全删) */
|
||||
int deleteByMeetingId(Long meetingId);
|
||||
|
||||
/** 批量插入 (单会议替换 save 专用) */
|
||||
int insertBatch(List<BizMeetingMaterial> list);
|
||||
|
||||
/** 单条更新 amount (OCR 识别为发票后回写, 不动其他字段) */
|
||||
int updateAmount(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingSupervisor;
|
||||
|
||||
/**
|
||||
* 会议-监察员 Mapper 接口
|
||||
*/
|
||||
public interface BizMeetingSupervisorMapper {
|
||||
|
||||
/** 按主键查 */
|
||||
BizMeetingSupervisor selectByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID查该会议的所有监察员 */
|
||||
List<BizMeetingSupervisor> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 按 userId 查该监察员被分配到哪些会议 */
|
||||
List<BizMeetingSupervisor> selectByUserId(Long userId);
|
||||
|
||||
/** 条件查询 */
|
||||
List<BizMeetingSupervisor> selectList(BizMeetingSupervisor entity);
|
||||
|
||||
/** 插入 (id AUTO_INCREMENT) */
|
||||
int insert(BizMeetingSupervisor entity);
|
||||
|
||||
/** 按主键更新 */
|
||||
int updateByPrimaryKey(BizMeetingSupervisor entity);
|
||||
|
||||
/** 按主键删除 */
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
/** 按会议ID全删 (分配时全删全插) */
|
||||
int deleteByMeetingId(Long meetingId);
|
||||
|
||||
/** 批量插入 */
|
||||
int insertBatch(List<BizMeetingSupervisor> list);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 结构化发票字段 */
|
||||
@Data
|
||||
public class InvoiceFields {
|
||||
/** 发票类型:增值税电子普通发票 / 增值税专用发票 / ... */
|
||||
private String invoiceType;
|
||||
/** 发票号码 */
|
||||
private String invoiceNo;
|
||||
/** 发票代码 */
|
||||
private String invoiceCode;
|
||||
/** 开票日期 YYYY-MM-DD */
|
||||
private String invoiceDate;
|
||||
|
||||
/** 价税合计(小写) */
|
||||
private Double amount;
|
||||
/** 价税合计(大写中文) */
|
||||
private String amountCn;
|
||||
/** 不含税金额 */
|
||||
private Double amountPretax;
|
||||
/** 税额 */
|
||||
private Double taxAmount;
|
||||
|
||||
/** 销售方名称 */
|
||||
private String sellerName;
|
||||
/** 销售方纳税人识别号 */
|
||||
private String sellerTaxNo;
|
||||
/** 购买方名称 */
|
||||
private String buyerName;
|
||||
/** 购买方纳税人识别号 */
|
||||
private String buyerTaxNo;
|
||||
|
||||
/** 大写金额 vs 小写金额是否一致(null=未能比对) */
|
||||
private Boolean amountMatch;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import com.ruoyi.business.domain.BizMeetingInvoice;
|
||||
import com.ruoyi.business.mapper.BizMeetingInvoiceMapper;
|
||||
import com.ruoyi.business.service.impl.InvoiceOcrService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* OCR 兜底调度器
|
||||
* <p>
|
||||
* 每 60 秒扫描一次, 重新识别 5 分钟前插入但仍未识别的 invoice 行
|
||||
* (处理: 程序重启导致后台任务丢失 / OCR 服务临时挂掉 / OSS 下载超时 等情况)
|
||||
* <p>
|
||||
* 需要启动类加 {@code @EnableScheduling} 才会生效 (RuoyiApplication 已有)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class InvoiceOcrScheduler
|
||||
{
|
||||
/** 阈值: UNRECOGNIZED 行超过这个分钟数才重试 (避免抢正在跑的 OCR 任务) */
|
||||
private static final int STALE_MINUTES = 5;
|
||||
|
||||
@Autowired
|
||||
private BizMeetingInvoiceMapper invoiceMapper;
|
||||
|
||||
@Autowired
|
||||
private InvoiceOcrService ocrService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("ocrExecutor")
|
||||
private ExecutorService ocrExecutor;
|
||||
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void scanStaleUnrecognized()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<BizMeetingInvoice> stale = invoiceMapper.selectUnrecognizedOlderThanMinutes(STALE_MINUTES);
|
||||
if (stale == null || stale.isEmpty()) return;
|
||||
log.info("兜底扫描: {} 条 UNRECOGNIZED 超过 {} 分钟, 重新提交 OCR", stale.size(), STALE_MINUTES);
|
||||
for (BizMeetingInvoice inv : stale)
|
||||
{
|
||||
ocrExecutor.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
ocrService.recognizeOneInvoice(inv);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("兜底 OCR 失败 id={} url={}", inv.getId(), inv.getOssUrl(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("兜底扫描异常 (本次跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** OCR 识别结果(与 ry-ocr 的 InvoiceResult JSON 对应) */
|
||||
@Data
|
||||
public class InvoiceResult {
|
||||
private Boolean success;
|
||||
private String rawText;
|
||||
private String engine;
|
||||
private Integer pageCount;
|
||||
private Integer elapsedMs;
|
||||
private String error;
|
||||
private InvoiceFields fields;
|
||||
private List<OcrLine> lines;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* ry-ocr Java 调用客户端
|
||||
*
|
||||
* 依赖:hutool-http, hutool-json, hutool-core, lombok
|
||||
*
|
||||
* 用法:
|
||||
* OcrClient client = new OcrClient("http://127.0.0.1:8801");
|
||||
* InvoiceResult r = client.recognize(new File("d:/发票.pdf"));
|
||||
* InvoiceResult r2 = client.recognizeByUrl("https://oss.example.com/xxx.png");
|
||||
* System.out.println(r.getFields().getAmount());
|
||||
*/
|
||||
@Slf4j
|
||||
public class OcrClient {
|
||||
|
||||
private final String baseUrl;
|
||||
|
||||
public OcrClient(String baseUrl) {
|
||||
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
}
|
||||
|
||||
/** 健康检查 */
|
||||
public boolean ping() {
|
||||
try (HttpResponse resp = HttpRequest.get(baseUrl + "/health").timeout(3000).execute()) {
|
||||
return resp.getStatus() == 200 && "ok".equals(JSONUtil.parseObj(resp.body()).getStr("status"));
|
||||
} catch (Exception e) {
|
||||
log.warn("ocr ping failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 识别发票(图片或 PDF) */
|
||||
public InvoiceResult recognize(File file) {
|
||||
try (HttpResponse resp = HttpRequest.post(baseUrl + "/recognize/invoice")
|
||||
.form("file", file)
|
||||
.timeout(60_000)
|
||||
.execute()) {
|
||||
|
||||
String body = resp.body();
|
||||
JSONObject json = JSONUtil.parseObj(body);
|
||||
if (resp.getStatus() != 200) {
|
||||
throw new RuntimeException("OCR 调用失败: " + resp.getStatus() + " " + body);
|
||||
}
|
||||
return parse(json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 识别发票: 后端下载 OSS URL 到临时文件 → recognize → 清理临时文件.
|
||||
* 临时文件目录: System.getProperty("java.io.tmpdir")/ry-ocr/
|
||||
*
|
||||
* @param url OSS 可访问 URL
|
||||
* @return 识别结果
|
||||
*/
|
||||
public InvoiceResult recognizeByUrl(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
throw new IllegalArgumentException("ossUrl 不能为空");
|
||||
}
|
||||
File tmpDir = new File(System.getProperty("java.io.tmpdir"), "ry-ocr");
|
||||
if (!tmpDir.exists() && !tmpDir.mkdirs()) {
|
||||
throw new RuntimeException("无法创建临时目录: " + tmpDir.getAbsolutePath());
|
||||
}
|
||||
// 从 URL 截取文件名, 保留后缀 (用于 ry-ocr 推断图片/PDF)
|
||||
String name = url.substring(url.lastIndexOf('/') + 1);
|
||||
if (name.indexOf('?') >= 0) name = name.substring(0, name.indexOf('?'));
|
||||
if (name.indexOf('.') < 0) name = name + ".png";
|
||||
File tmp = new File(tmpDir, System.currentTimeMillis() + "_" + name);
|
||||
try {
|
||||
long size = HttpUtil.downloadFile(url, tmp);
|
||||
if (size <= 0) {
|
||||
throw new RuntimeException("OSS 文件下载失败或为空: " + url);
|
||||
}
|
||||
log.info("OCR 下载: url={} size={}B tmp={}", url, size, tmp.getAbsolutePath());
|
||||
return recognize(tmp);
|
||||
} finally {
|
||||
FileUtil.del(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
private InvoiceResult parse(JSONObject json) {
|
||||
InvoiceResult r = new InvoiceResult();
|
||||
r.setSuccess(json.getBool("success", false));
|
||||
r.setRawText(json.getStr("rawText", ""));
|
||||
r.setEngine(json.getStr("engine", ""));
|
||||
r.setPageCount(json.getInt("pageCount", 1));
|
||||
r.setElapsedMs(json.getInt("elapsedMs", 0));
|
||||
r.setError(json.getStr("error"));
|
||||
|
||||
JSONObject f = json.getJSONObject("fields");
|
||||
if (f != null) {
|
||||
InvoiceFields fields = new InvoiceFields();
|
||||
fields.setInvoiceType(f.getStr("invoiceType"));
|
||||
fields.setInvoiceNo(f.getStr("invoiceNo"));
|
||||
fields.setInvoiceCode(f.getStr("invoiceCode"));
|
||||
fields.setInvoiceDate(f.getStr("invoiceDate"));
|
||||
fields.setAmount(f.getDouble("amount"));
|
||||
fields.setAmountCn(f.getStr("amountCn"));
|
||||
fields.setAmountPretax(f.getDouble("amount_pretax"));
|
||||
fields.setTaxAmount(f.getDouble("taxAmount"));
|
||||
fields.setSellerName(f.getStr("sellerName"));
|
||||
fields.setSellerTaxNo(f.getStr("sellerTaxNo"));
|
||||
fields.setBuyerName(f.getStr("buyerName"));
|
||||
fields.setBuyerTaxNo(f.getStr("buyerTaxNo"));
|
||||
fields.setAmountMatch(f.getBool("amountMatch"));
|
||||
r.setFields(fields);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 单行 OCR 识别结果 */
|
||||
@Data
|
||||
public class OcrLine {
|
||||
private String text;
|
||||
private Double confidence;
|
||||
private List<List<Double>> box;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ruoyi.business.ocr;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* 下载 zip 到临时目录, 解压, 返回所有图片/PDF 文件
|
||||
* <p>
|
||||
* 临时目录由调用方识别完成后调用 {@link #cleanup(File)} 清理
|
||||
* <p>
|
||||
* 依赖: JDK 自带 {@link ZipInputStream} (不引 commons-compress / zip4j)
|
||||
*/
|
||||
@Slf4j
|
||||
public class ZipExtractor
|
||||
{
|
||||
/**
|
||||
* @param zipUrl zip 的 OSS URL
|
||||
* @return 解压后的 File 列表 (仅 .png/.jpg/.jpeg/.pdf, 子目录展平, 用 _ 拼接)
|
||||
* @throws IOException 下载失败 / IO 异常
|
||||
*/
|
||||
public static List<File> extract(String zipUrl) throws IOException
|
||||
{
|
||||
File tmpRoot = new File(System.getProperty("java.io.tmpdir"),
|
||||
"ry-ocr-zip/" + System.currentTimeMillis());
|
||||
if (!tmpRoot.mkdirs()) throw new IOException("无法创建临时目录: " + tmpRoot);
|
||||
File zipFile = new File(tmpRoot, "input.zip");
|
||||
try
|
||||
{
|
||||
long size = HttpUtil.downloadFile(zipUrl, zipFile);
|
||||
if (size <= 0) throw new IOException("OSS zip 下载失败: " + zipUrl);
|
||||
log.info("zip 下载: url={} size={}B tmp={}", zipUrl, size, zipFile.getAbsolutePath());
|
||||
|
||||
List<File> out = new ArrayList<>();
|
||||
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile)))
|
||||
{
|
||||
ZipEntry e;
|
||||
while ((e = zin.getNextEntry()) != null)
|
||||
{
|
||||
if (e.isDirectory()) continue;
|
||||
String name = e.getName();
|
||||
String lower = name.toLowerCase();
|
||||
if (!(lower.endsWith(".png") || lower.endsWith(".jpg")
|
||||
|| lower.endsWith(".jpeg") || lower.endsWith(".pdf")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 子目录展平: a/b/c.png → tmpRoot/a_b_c.png
|
||||
File outFile = new File(tmpRoot, name.replace("/", "_"));
|
||||
try (FileOutputStream fos = new FileOutputStream(outFile))
|
||||
{
|
||||
zin.transferTo(fos);
|
||||
}
|
||||
out.add(outFile);
|
||||
}
|
||||
}
|
||||
log.info("zip 解压完成: 文件数={}", out.size());
|
||||
return out;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// zip 压缩包本身删掉;解压产物在 tmpRoot 下,等识别完由 cleanup 删
|
||||
if (zipFile.exists()) zipFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理整个解压临时目录 */
|
||||
public static void cleanup(File tmpRoot)
|
||||
{
|
||||
if (tmpRoot != null && tmpRoot.exists())
|
||||
{
|
||||
FileUtil.del(tmpRoot);
|
||||
log.info("清理 zip 临时目录: {}", tmpRoot.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.business.oss;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.config.RuoYiConfig.OssProperties;
|
||||
|
||||
/**
|
||||
* OSS 连接配置 (copy 自 hwt-serve/ruoyi-common-base/.../third/meta/OssConfMeta)
|
||||
* <p>
|
||||
* 改造点:
|
||||
* <ul>
|
||||
* <li>去掉了 hwt-serve 的 @Value("${ali.*}") 硬编码配置,改读本项目 {@link RuoYiConfig.OssProperties}</li>
|
||||
* <li>endpoint 在 application.yml 里带 https:// 前缀 (例: https://hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com),
|
||||
* 但 {@code OSSClient} 构造时只吃裸 host → {@link #stripScheme(String)} 剥前缀</li>
|
||||
* <li>不创建 bucket (hwt-serve 的 getOSSClient() 会自动建,本项目 bucket 已存在,无需建)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
public class OssConfMeta
|
||||
{
|
||||
private final String endpoint;
|
||||
private final String bucket;
|
||||
private final String accessKeyId;
|
||||
private final String accessKeySecret;
|
||||
|
||||
public OssConfMeta(RuoYiConfig cfg)
|
||||
{
|
||||
OssProperties p = cfg.getOss();
|
||||
if (p == null)
|
||||
{
|
||||
throw new IllegalStateException("OSS 未配置 (application.yml 缺 ruoyi.oss.*)");
|
||||
}
|
||||
this.endpoint = stripScheme(p.getEndpoint());
|
||||
this.bucket = p.getBucket();
|
||||
this.accessKeyId = p.getAccessKeyId();
|
||||
this.accessKeySecret = p.getAccessKeySecret();
|
||||
}
|
||||
|
||||
/**
|
||||
* 剥协议头与可能的 path
|
||||
* 例: https://hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com → hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com
|
||||
*/
|
||||
private static String stripScheme(String url)
|
||||
{
|
||||
if (url == null) return null;
|
||||
String s = url;
|
||||
if (s.startsWith("https://")) s = s.substring("https://".length());
|
||||
else if (s.startsWith("http://")) s = s.substring("http://".length());
|
||||
int slash = s.indexOf('/');
|
||||
return slash >= 0 ? s.substring(0, slash) : s;
|
||||
}
|
||||
|
||||
public String getEndpoint() { return endpoint; }
|
||||
public String getBucket() { return bucket; }
|
||||
public String getAccessKeyId() { return accessKeyId; }
|
||||
public String getAccessKeySecret() { return accessKeySecret; }
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.ruoyi.business.oss;
|
||||
|
||||
import com.aliyun.oss.OSSClient;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 服务端 OSS 上传器 (copy 自 hwt-serve/ruoyi-common-base/.../third/AliOssService,精简)
|
||||
* <p>
|
||||
* 用于: ZIP 解压 → OCR 识别为发票 → 重新上传到 OSS,拿新 URL 写入 invoice.oss_url
|
||||
* (不传 zip 的 oss_url, 因为那是压缩包不是发票本体)
|
||||
* <p>
|
||||
* 与 hwt-serve 的差异:
|
||||
* <ul>
|
||||
* <li>只保留 {@code upload} (服务端上传) 一个公开方法;删除 deleteFile / extractKey (本场景不需要)</li>
|
||||
* <li>key 前缀由调用方通过 {@code subDir} 传入 (例 "meeting/invoice"),不再硬编码 "files/yyyy/MM/dd/"</li>
|
||||
* <li>Content-Type / Content-Disposition 复用 hwt-serve 的 guessContentType 实现 (含 bmp/jpg/doc/ppt/pdf)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class OssUploader
|
||||
{
|
||||
@Autowired
|
||||
private OssConfMeta ossConfMeta;
|
||||
|
||||
/**
|
||||
* 上传字节到 OSS
|
||||
*
|
||||
* @param data 文件字节
|
||||
* @param originalFilename 原始文件名 (用于决定 Content-Type 和 URL 末尾,不是 OSS key)
|
||||
* @param subDir 业务子目录,直接拼在 dirPrefix 后 (例 "meeting/invoice")
|
||||
* @return 完整 URL: {@code https://{bucket}.{endpoint}/{key}}
|
||||
*/
|
||||
public String upload(byte[] data, String originalFilename, String subDir)
|
||||
{
|
||||
if (data == null || data.length == 0) throw new IllegalArgumentException("上传字节为空");
|
||||
if (originalFilename == null) throw new IllegalArgumentException("originalFilename 不能为空");
|
||||
log.info("OSS 上传开始: name={} size={}B subDir={}", originalFilename, data.length, subDir);
|
||||
|
||||
String ext = originalFilename.contains(".")
|
||||
? originalFilename.substring(originalFilename.lastIndexOf('.'))
|
||||
: "";
|
||||
String date = new SimpleDateFormat("yyyyMM").format(new Date());
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
String key = (subDir == null ? "" : subDir + "/") + date + "/" + uuid + ext;
|
||||
|
||||
OSSClient client = new OSSClient(
|
||||
ossConfMeta.getEndpoint(),
|
||||
ossConfMeta.getAccessKeyId(),
|
||||
ossConfMeta.getAccessKeySecret());
|
||||
try
|
||||
{
|
||||
ObjectMetadata meta = buildObjectMeta(originalFilename, ext);
|
||||
client.putObject(ossConfMeta.getBucket(), key, new ByteArrayInputStream(data), meta);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
String url = "https://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/" + key;
|
||||
log.info("OSS 上传完成: {}", url);
|
||||
return url;
|
||||
}
|
||||
|
||||
private static ObjectMetadata buildObjectMeta(String filename, String ext)
|
||||
{
|
||||
ObjectMetadata meta = new ObjectMetadata();
|
||||
meta.setCacheControl("no-cache");
|
||||
meta.setHeader("Pragma", "no-cache");
|
||||
meta.setContentType(guessContentType(ext));
|
||||
meta.setContentDisposition("inline;filename=" + filename);
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制自 hwt-serve AliOssService.guessContentType
|
||||
* 注: hwt-serve 把所有图片 (bmp/jpg/png) 都映射为 image/jpg,与命名不太严谨
|
||||
* 本实现按标准 mime 区分 png / jpg / jpeg
|
||||
*/
|
||||
private static String guessContentType(String ext)
|
||||
{
|
||||
if (ext == null) return "application/octet-stream";
|
||||
String lower = ext.toLowerCase();
|
||||
if (".bmp".equals(lower)) return "image/bmp";
|
||||
if (".png".equals(lower)) return "image/png";
|
||||
if (".jpg".equals(lower) || ".jpeg".equals(lower)) return "image/jpeg";
|
||||
if (".gif".equals(lower)) return "image/gif";
|
||||
if (".webp".equals(lower)) return "image/webp";
|
||||
if (".pdf".equals(lower)) return "application/pdf";
|
||||
if (".doc".equals(lower) || ".docx".equals(lower)) return "application/msword";
|
||||
if (".ppt".equals(lower) || ".pptx".equals(lower)) return "application/vnd.ms-powerpoint";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingAuditLog;
|
||||
|
||||
/**
|
||||
* 会议审核流程日志 Service 接口
|
||||
*/
|
||||
public interface IBizMeetingAuditLogService {
|
||||
|
||||
BizMeetingAuditLog getById(Long id);
|
||||
|
||||
List<BizMeetingAuditLog> selectList(BizMeetingAuditLog entity);
|
||||
|
||||
int insert(BizMeetingAuditLog entity);
|
||||
|
||||
int updateByPrimaryKey(BizMeetingAuditLog entity);
|
||||
|
||||
int deleteByPrimaryKey(Long id);
|
||||
|
||||
int deleteByPrimaryKeys(Long[] ids);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingExecutor;
|
||||
|
||||
/**
|
||||
* 会议-执行人员 Service 接口
|
||||
*/
|
||||
public interface IBizMeetingExecutorService {
|
||||
|
||||
BizMeetingExecutor getById(Long id);
|
||||
|
||||
List<BizMeetingExecutor> selectByMeetingId(Long meetingId);
|
||||
|
||||
List<BizMeetingExecutor> selectByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 替换该会议的执行人员 (全删全插, 一个事务)
|
||||
* @param assignedBy 分配人 user_id
|
||||
*/
|
||||
int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingInvoice;
|
||||
|
||||
/**
|
||||
* 会议发票 Service 接口
|
||||
*/
|
||||
public interface IBizMeetingInvoiceService {
|
||||
|
||||
BizMeetingInvoice getById(Long id);
|
||||
|
||||
BizMeetingInvoice selectByMaterialId(Long materialId);
|
||||
|
||||
List<BizMeetingInvoice> selectByMeetingId(Long meetingId);
|
||||
|
||||
/**
|
||||
* Upsert: 按 material_id 唯一 (uk_material), 存在则更新金额, 不存在则插入.
|
||||
* 仅在 OCR 识别为发票时调用, 不是发票不调此方法 (不入库).
|
||||
*/
|
||||
int upsertByMaterialId(BizMeetingInvoice record);
|
||||
|
||||
int deleteByMaterialId(Long materialId);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
|
||||
/**
|
||||
* 会议材料 Service 接口
|
||||
*/
|
||||
public interface IBizMeetingMaterialService {
|
||||
|
||||
/** 按主键查单条 */
|
||||
BizMeetingMaterial getById(Long id);
|
||||
|
||||
/** 按会议ID查该会议所有材料记录 */
|
||||
List<BizMeetingMaterial> selectByMeetingId(Long meetingId);
|
||||
|
||||
/**
|
||||
* 替换该会议的所有材料记录 (一个事务, 全删全插)
|
||||
* <p>
|
||||
* 用于"保存"按钮: 前端传当前 UI 上传的文件列表, 后端先删后插.
|
||||
* <ul>
|
||||
* <li>list 为空或 null → 仅删除该会议的全部记录, 不插入</li>
|
||||
* <li>list 非空 → 先 deleteByMeetingId, 再 insertBatch</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return 插入后带 id 的 list (前端可借此触发 OCR 识别, 拿到 materialId 关联 invoice 表)
|
||||
*/
|
||||
List<BizMeetingMaterial> replaceByMeetingId(Long meetingId, List<BizMeetingMaterial> list);
|
||||
|
||||
/**
|
||||
* 单条更新 amount (OCR 识别为发票后回写).
|
||||
* 不动其他字段, 不抛异常 (失败仅 log).
|
||||
*/
|
||||
int updateAmount(Long materialId, java.math.BigDecimal amount);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.business.domain.BizMeetingSupervisor;
|
||||
|
||||
/**
|
||||
* 会议-监察员 Service 接口
|
||||
*/
|
||||
public interface IBizMeetingSupervisorService {
|
||||
|
||||
BizMeetingSupervisor getById(Long id);
|
||||
|
||||
List<BizMeetingSupervisor> selectByMeetingId(Long meetingId);
|
||||
|
||||
List<BizMeetingSupervisor> selectByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 替换该会议的监察员 (全删全插, 一个事务)
|
||||
* @param assignedBy 分配人 user_id (前端 / manager 自己)
|
||||
*/
|
||||
int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.business.domain.BizMeetingAuditLog;
|
||||
import com.ruoyi.business.mapper.BizMeetingAuditLogMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingAuditLogService;
|
||||
|
||||
@Service
|
||||
public class BizMeetingAuditLogServiceImpl implements IBizMeetingAuditLogService {
|
||||
|
||||
@Autowired
|
||||
private BizMeetingAuditLogMapper bizMeetingAuditLogMapper;
|
||||
|
||||
@Override
|
||||
public BizMeetingAuditLog getById(Long id) {
|
||||
return bizMeetingAuditLogMapper.selectByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingAuditLog> selectList(BizMeetingAuditLog entity) {
|
||||
return bizMeetingAuditLogMapper.selectList(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insert(BizMeetingAuditLog entity) {
|
||||
return bizMeetingAuditLogMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateByPrimaryKey(BizMeetingAuditLog entity) {
|
||||
return bizMeetingAuditLogMapper.updateByPrimaryKey(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKey(Long id) {
|
||||
return bizMeetingAuditLogMapper.deleteByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKeys(Long[] ids) {
|
||||
return bizMeetingAuditLogMapper.deleteByPrimaryKeys(ids);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeetingExecutor;
|
||||
import com.ruoyi.business.mapper.BizMeetingExecutorMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingExecutorService;
|
||||
|
||||
@Service
|
||||
public class BizMeetingExecutorServiceImpl implements IBizMeetingExecutorService {
|
||||
|
||||
@Autowired
|
||||
private BizMeetingExecutorMapper bizMeetingExecutorMapper;
|
||||
|
||||
@Override
|
||||
public BizMeetingExecutor getById(Long id) {
|
||||
return bizMeetingExecutorMapper.selectByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingExecutor> selectByMeetingId(Long meetingId) {
|
||||
return bizMeetingExecutorMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingExecutor> selectByUserId(Long userId) {
|
||||
return bizMeetingExecutorMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy) {
|
||||
bizMeetingExecutorMapper.deleteByMeetingId(meetingId);
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<BizMeetingExecutor> list = new ArrayList<>(userIds.size());
|
||||
for (Long uid : userIds) {
|
||||
BizMeetingExecutor m = new BizMeetingExecutor();
|
||||
m.setMeetingId(meetingId);
|
||||
m.setUserId(uid);
|
||||
m.setAssignedBy(assignedBy);
|
||||
list.add(m);
|
||||
}
|
||||
return bizMeetingExecutorMapper.insertBatch(list);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeetingInvoice;
|
||||
import com.ruoyi.business.mapper.BizMeetingInvoiceMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingInvoiceService;
|
||||
|
||||
@Service
|
||||
public class BizMeetingInvoiceServiceImpl implements IBizMeetingInvoiceService {
|
||||
|
||||
@Autowired
|
||||
private BizMeetingInvoiceMapper bizMeetingInvoiceMapper;
|
||||
|
||||
@Override
|
||||
public BizMeetingInvoice getById(Long id) {
|
||||
return bizMeetingInvoiceMapper.selectByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BizMeetingInvoice selectByMaterialId(Long materialId) {
|
||||
return bizMeetingInvoiceMapper.selectByMaterialId(materialId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingInvoice> selectByMeetingId(Long meetingId) {
|
||||
return bizMeetingInvoiceMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert: 存在则按 material_id 更新 (amount + url + 时间), 不存在则插入新行.
|
||||
* <p>
|
||||
* 注意: UK uk_material 保证一个 material_id 只对应一条 invoice, 自然幂等.
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int upsertByMaterialId(BizMeetingInvoice record) {
|
||||
if (record == null || record.getMaterialId() == null) return 0;
|
||||
Date now = new Date();
|
||||
BizMeetingInvoice exist = bizMeetingInvoiceMapper.selectByMaterialId(record.getMaterialId());
|
||||
if (exist == null) {
|
||||
record.setId(null);
|
||||
if (record.getCreateTime() == null) record.setCreateTime(now);
|
||||
record.setUpdateTime(now);
|
||||
return bizMeetingInvoiceMapper.insert(record);
|
||||
} else {
|
||||
// 更新金额/url, 时间戳
|
||||
if (record.getAmount() != null) exist.setAmount(record.getAmount());
|
||||
if (record.getOssUrl() != null && !record.getOssUrl().isEmpty()) exist.setOssUrl(record.getOssUrl());
|
||||
exist.setUpdateTime(now);
|
||||
return bizMeetingInvoiceMapper.updateByPrimaryKey(exist);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByMaterialId(Long materialId) {
|
||||
return bizMeetingInvoiceMapper.deleteByMaterialId(materialId);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
|
||||
@Service
|
||||
public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService {
|
||||
|
||||
@Autowired
|
||||
private BizMeetingMaterialMapper bizMeetingMaterialMapper;
|
||||
|
||||
@Override
|
||||
public BizMeetingMaterial getById(Long id) {
|
||||
return bizMeetingMaterialMapper.selectByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingMaterial> selectByMeetingId(Long meetingId) {
|
||||
return bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全删全插, 一个事务. list 为空则只删不插.
|
||||
* <p>
|
||||
* 防御性捕获 DuplicateKeyException: 正常流程 (delete → insert) 不会触发,
|
||||
* 但并发或前端传重复 (meetingId, materialType, subType) 会触发 UK uk_meeting_type_sub.
|
||||
* 翻译成友好中文提示, 避免暴露 SQL 堆栈.
|
||||
* <p>
|
||||
* 返回插入后的 list (各元素 id 字段被 useGeneratedKeys 回填), 前端可借此触发 OCR.
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<BizMeetingMaterial> replaceByMeetingId(Long meetingId, List<BizMeetingMaterial> list) {
|
||||
bizMeetingMaterialMapper.deleteByMeetingId(meetingId);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
// 不接受前端传的 id, 走 AUTO_INCREMENT; meetingId 兜底由路径提供
|
||||
for (BizMeetingMaterial m : list) {
|
||||
m.setId(null);
|
||||
m.setMeetingId(meetingId);
|
||||
}
|
||||
try {
|
||||
bizMeetingMaterialMapper.insertBatch(list);
|
||||
} catch (DuplicateKeyException e) {
|
||||
throw new ServiceException("材料上传重复, 请检查 (同一会议下同一资料类型同一子分类只能有一条记录)");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateAmount(Long materialId, java.math.BigDecimal amount) {
|
||||
if (materialId == null || amount == null) return 0;
|
||||
return bizMeetingMaterialMapper.updateAmount(materialId, amount);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeetingSupervisor;
|
||||
import com.ruoyi.business.mapper.BizMeetingSupervisorMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingSupervisorService;
|
||||
|
||||
@Service
|
||||
public class BizMeetingSupervisorServiceImpl implements IBizMeetingSupervisorService {
|
||||
|
||||
@Autowired
|
||||
private BizMeetingSupervisorMapper bizMeetingSupervisorMapper;
|
||||
|
||||
@Override
|
||||
public BizMeetingSupervisor getById(Long id) {
|
||||
return bizMeetingSupervisorMapper.selectByPrimaryKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingSupervisor> selectByMeetingId(Long meetingId) {
|
||||
return bizMeetingSupervisorMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingSupervisor> selectByUserId(Long userId) {
|
||||
return bizMeetingSupervisorMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy) {
|
||||
bizMeetingSupervisorMapper.deleteByMeetingId(meetingId);
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<BizMeetingSupervisor> list = new ArrayList<>(userIds.size());
|
||||
for (Long uid : userIds) {
|
||||
BizMeetingSupervisor m = new BizMeetingSupervisor();
|
||||
m.setMeetingId(meetingId);
|
||||
m.setUserId(uid);
|
||||
m.setAssignedBy(assignedBy);
|
||||
list.add(m);
|
||||
}
|
||||
return bizMeetingSupervisorMapper.insertBatch(list);
|
||||
}
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.business.service.IBizMeetingInvoiceService;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import com.ruoyi.business.domain.BizMeetingInvoice;
|
||||
import com.ruoyi.business.mapper.BizMeetingInvoiceMapper;
|
||||
import com.ruoyi.business.ocr.InvoiceFields;
|
||||
import com.ruoyi.business.ocr.InvoiceResult;
|
||||
import com.ruoyi.business.ocr.OcrClient;
|
||||
import com.ruoyi.business.ocr.ZipExtractor;
|
||||
import com.ruoyi.business.oss.OssUploader;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
|
||||
/**
|
||||
* 发票识别协调服务 (v3 重写)
|
||||
* <p>
|
||||
* 设计要点 (v2 → v3 变更):
|
||||
* <ul>
|
||||
* <li><b>后台执行</b>: 单文件 OCR 改为 {@code ExecutorService.submit}, 立即返回 SUBMITTED
|
||||
* (原 v2 是同步阻塞, 大文件/慢网络时拖慢保存接口)</li>
|
||||
* <li><b>ZIP 支持</b>: zipUrl → 下载 → 解压 → 遍历 .png/.jpg/.jpeg/.pdf →
|
||||
* 是发票 → <b>重新上传到 OSS</b> (key 是新生成的, 不是 zip 的 url) → 写 invoice 行</li>
|
||||
* <li><b>is_invoice 列</b>: <b>不加</b>. 不是发票的图直接不入 invoice 表</li>
|
||||
* <li><b>替换场景</b>: 前端传 {@code oldMaterialId}, 后端先 DELETE invoice WHERE material_id=old
|
||||
* + material.amount=0, 再走 OCR</li>
|
||||
* <li><b>状态机</b>: UNRECOGNIZED → RECOGNIZED / FAILED, 兜底 scheduler 每 60s 重试 > 5min 未识别行</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
public class InvoiceOcrService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(InvoiceOcrService.class);
|
||||
|
||||
@Autowired
|
||||
private OcrClient ocrClient;
|
||||
|
||||
@Autowired
|
||||
private OssUploader ossUploader;
|
||||
|
||||
@Autowired
|
||||
private BizMeetingInvoiceMapper invoiceMapper;
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingInvoiceService invoiceService;
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingMaterialService materialService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("ocrExecutor")
|
||||
private ExecutorService ocrExecutor;
|
||||
|
||||
// ==================== 对外入口 ====================
|
||||
|
||||
/**
|
||||
* 提交识别 (后台执行, 立即返回 SUBMITTED)
|
||||
*
|
||||
* @param materialId biz_meeting_material.id
|
||||
* @param meetingId 会议 ID
|
||||
* @param ossUrl OSS URL (单文件: 原图;ZIP: zip 包)
|
||||
* @param isZip true → ZIP 路径;false → 单文件路径
|
||||
* @param oldMaterialId 替换场景携带, 后端先 DELETE invoice WHERE material_id=old + material.amount=0;
|
||||
* null → 新增场景, 不做清理
|
||||
* @return 提交摘要 (立即返回, 不等 OCR 完成)
|
||||
*/
|
||||
public RecognizeResult submitRecognition(Long materialId, Long meetingId, String ossUrl,
|
||||
boolean isZip, Long oldMaterialId)
|
||||
{
|
||||
RecognizeResult out = new RecognizeResult();
|
||||
if (materialId == null || meetingId == null || ossUrl == null || ossUrl.isEmpty())
|
||||
{
|
||||
out.setSubmitted(false);
|
||||
out.setErrorMsg("参数缺失");
|
||||
return out;
|
||||
}
|
||||
|
||||
// 1. 替换场景: 先清旧 (deleteByMaterialId + amount=0)
|
||||
if (oldMaterialId != null)
|
||||
{
|
||||
log.info("替换场景: 清旧 invoice + material.amount=0, oldMaterialId={}", oldMaterialId);
|
||||
invoiceMapper.deleteByMaterialId(oldMaterialId);
|
||||
materialService.updateAmount(oldMaterialId, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 2. 单文件: 插 UNRECOGNIZED 占位行 (zip 路径跳过 — 解压才知道有几张)
|
||||
if (!isZip)
|
||||
{
|
||||
BizMeetingInvoice placeholder = new BizMeetingInvoice();
|
||||
placeholder.setMeetingId(meetingId);
|
||||
placeholder.setMaterialId(materialId);
|
||||
placeholder.setOssUrl(ossUrl);
|
||||
placeholder.setInvoiceType("SUB");
|
||||
placeholder.setRecognizeStatus("UNRECOGNIZED");
|
||||
placeholder.setCreatorId(SecurityUtils.getUserId());
|
||||
placeholder.setCreateTime(new Date());
|
||||
placeholder.setUpdateTime(new Date());
|
||||
invoiceMapper.insert(placeholder);
|
||||
}
|
||||
|
||||
// 3. 后台执行 OCR (单文件或 zip)
|
||||
final String url = ossUrl;
|
||||
ocrExecutor.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isZip)
|
||||
{
|
||||
recognizeZip(materialId, meetingId, url);
|
||||
}
|
||||
else
|
||||
{
|
||||
recognizeSingle(materialId, url);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("OCR 后台任务失败 materialId={} isZip={} err={}", materialId, isZip, e.getMessage(), e);
|
||||
if (!isZip)
|
||||
{
|
||||
// 单文件: 占位行标 FAILED
|
||||
invoiceMapper.updateStatusByMaterial(materialId, "FAILED",
|
||||
e.getMessage() == null ? "OCR 异常" : e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
out.setSubmitted(true);
|
||||
out.setIsZip(isZip);
|
||||
out.setMaterialId(materialId);
|
||||
out.setStatus("SUBMITTED");
|
||||
return out;
|
||||
}
|
||||
|
||||
// ==================== 后台执行方法 ====================
|
||||
|
||||
/**
|
||||
* 单文件 OCR:
|
||||
* 下载 → 识别 → 是发票 → updateStatus=RECOGNIZED + 回写 amount + material.amount
|
||||
* 不是发票 → 删除占位行 (material 表不动, amount 保持原值)
|
||||
* <p>
|
||||
* material 表语义是"会议上传的文件", 不是发票也照样是上传文件, 不能动它
|
||||
*/
|
||||
void recognizeSingle(Long materialId, String ossUrl)
|
||||
{
|
||||
InvoiceResult ir = ocrClient.recognizeByUrl(ossUrl);
|
||||
if (Boolean.TRUE.equals(ir.getSuccess()) && ir.getFields() != null && isRecognizedAsInvoice(ir.getFields()))
|
||||
{
|
||||
BigDecimal amount = ir.getFields().getAmount() != null
|
||||
? BigDecimal.valueOf(ir.getFields().getAmount())
|
||||
: BigDecimal.ZERO;
|
||||
invoiceMapper.updateStatusByMaterial(materialId, "RECOGNIZED", null);
|
||||
invoiceMapper.updateAmountByMaterial(materialId, amount);
|
||||
materialService.updateAmount(materialId, amount);
|
||||
log.info("单文件识别成功: materialId={} amount={} elapsed={}ms",
|
||||
materialId, amount, ir.getElapsedMs());
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不是发票: 仅删占位行, material 表不动 (material 仍代表上传的文件本身)
|
||||
invoiceMapper.deleteByMaterialId(materialId);
|
||||
log.info("单文件识别非发票: materialId={} (invoiceType={}, amount={}) - material 表保持原状",
|
||||
materialId,
|
||||
ir.getFields() != null ? ir.getFields().getInvoiceType() : null,
|
||||
ir.getFields() != null ? ir.getFields().getAmount() : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ZIP OCR:
|
||||
* 下载 zip → 解压 → 遍历图片/PDF → 是发票 → 重传 OSS (新 URL) → 写 invoice 行 →
|
||||
* 累加 amount → material.amount = 总和
|
||||
* 不是发票 → 跳过 (不入 invoice 表, material 表不动)
|
||||
* <p>
|
||||
* material 表只动一次 (末尾的 updateAmount), 即使 0 张发票也照写; 非发票文件不进任何统计
|
||||
*/
|
||||
void recognizeZip(Long materialId, Long meetingId, String zipUrl) throws Exception
|
||||
{
|
||||
File tmpRoot = null;
|
||||
try
|
||||
{
|
||||
List<File> files = ZipExtractor.extract(zipUrl);
|
||||
if (!files.isEmpty())
|
||||
{
|
||||
tmpRoot = files.get(0).getParentFile();
|
||||
}
|
||||
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
int invoiceCount = 0;
|
||||
int skipCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (File f : files)
|
||||
{
|
||||
try
|
||||
{
|
||||
InvoiceResult ir = ocrClient.recognize(f);
|
||||
if (!Boolean.TRUE.equals(ir.getSuccess()) || ir.getFields() == null
|
||||
|| !isRecognizedAsInvoice(ir.getFields()))
|
||||
{
|
||||
// 不是发票: 跳过, 不入库 (不入 invoice 表, material 表不动)
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 修正点2: 是发票 → 重传 OSS, 用新 URL 写入 invoice 行 (不传 zipUrl)
|
||||
byte[] bytes = FileUtil.readBytes(f);
|
||||
String newUrl = ossUploader.upload(bytes, f.getName(), "meeting/invoice");
|
||||
|
||||
BigDecimal amount = ir.getFields().getAmount() != null
|
||||
? BigDecimal.valueOf(ir.getFields().getAmount())
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
BizMeetingInvoice inv = new BizMeetingInvoice();
|
||||
inv.setMeetingId(meetingId);
|
||||
inv.setMaterialId(materialId);
|
||||
inv.setOssUrl(newUrl);
|
||||
inv.setInvoiceType("SUB");
|
||||
inv.setAmount(amount);
|
||||
inv.setRecognizeStatus("RECOGNIZED");
|
||||
inv.setSourceFilename(f.getName());
|
||||
inv.setCreatorId(SecurityUtils.getUserId());
|
||||
inv.setCreateTime(new Date());
|
||||
inv.setUpdateTime(new Date());
|
||||
invoiceMapper.insert(inv);
|
||||
|
||||
total = total.add(amount);
|
||||
invoiceCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("zip 子文件识别失败 file={} err={}", f.getName(), e.getMessage());
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// material.amount 只在"识别出至少 1 张发票"时回写求和; 0 张时不动 material
|
||||
// (用户上传了文件, 即使全是合同照片, material 也应保留, amount 字段保持原值)
|
||||
if (invoiceCount > 0)
|
||||
{
|
||||
materialService.updateAmount(materialId, total);
|
||||
}
|
||||
log.info("zip OCR 完成 materialId={} invoice={} skip={} fail={} total={}",
|
||||
materialId, invoiceCount, skipCount, failCount, total);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ZipExtractor.cleanup(tmpRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底 OCR (InvoiceOcrScheduler 调用): 已知 invoice 行 (UNRECOGNIZED), 重新识别
|
||||
* <p>
|
||||
* 不是发票 → 仅删占位行, material 表不动 (与单文件/ZIP 路径一致)
|
||||
*/
|
||||
public void recognizeOneInvoice(BizMeetingInvoice inv)
|
||||
{
|
||||
if (inv == null || inv.getOssUrl() == null) return;
|
||||
try
|
||||
{
|
||||
InvoiceResult ir = ocrClient.recognizeByUrl(inv.getOssUrl());
|
||||
if (Boolean.TRUE.equals(ir.getSuccess()) && ir.getFields() != null && isRecognizedAsInvoice(ir.getFields()))
|
||||
{
|
||||
BigDecimal amount = ir.getFields().getAmount() != null
|
||||
? BigDecimal.valueOf(ir.getFields().getAmount())
|
||||
: BigDecimal.ZERO;
|
||||
invoiceMapper.updateStatusAndAmountByPrimaryKey(inv.getId(), "RECOGNIZED", amount, null);
|
||||
materialService.updateAmount(inv.getMaterialId(), amount);
|
||||
log.info("兜底识别成功: invoiceId={} amount={}", inv.getId(), amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不是发票: 仅删占位行, material 表保持原状
|
||||
invoiceMapper.deleteByPrimaryKey(inv.getId());
|
||||
log.info("兜底识别非发票: invoiceId={} 已删除 (material 表不动)", inv.getId());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
invoiceMapper.updateStatusAndAmountByPrimaryKey(inv.getId(), "FAILED", null,
|
||||
e.getMessage() == null ? "OCR 异常" : e.getMessage());
|
||||
log.warn("兜底识别失败 invoiceId={} err={}", inv.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 是否识别为发票: 必须有 invoiceType + amount>0
|
||||
* (ry-ocr 对非发票图片也可能返回 fields, 但 invoiceType/amount 通常为空/0)
|
||||
*/
|
||||
private boolean isRecognizedAsInvoice(InvoiceFields f)
|
||||
{
|
||||
if (f.getInvoiceType() == null || f.getInvoiceType().isEmpty()) return false;
|
||||
if (f.getAmount() == null || f.getAmount() <= 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== DTO ====================
|
||||
|
||||
/** 提交结果 (立即返回, 不等 OCR 完成) */
|
||||
public static class RecognizeResult
|
||||
{
|
||||
private boolean submitted;
|
||||
private boolean isZip;
|
||||
private Long materialId;
|
||||
private String status;
|
||||
private String errorMsg;
|
||||
|
||||
public boolean isSubmitted() { return submitted; }
|
||||
public void setSubmitted(boolean submitted) { this.submitted = submitted; }
|
||||
public boolean isZip() { return isZip; }
|
||||
public void setIsZip(boolean zip) { isZip = zip; }
|
||||
public Long getMaterialId() { return materialId; }
|
||||
public void setMaterialId(Long materialId) { this.materialId = materialId; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getErrorMsg() { return errorMsg; }
|
||||
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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.business.mapper.BizMeetingAuditLogMapper">
|
||||
|
||||
<resultMap type="BizMeetingAuditLog" id="BizMeetingAuditLogResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="auditor" column="auditor" />
|
||||
<result property="opinion" column="opinion" />
|
||||
<result property="currentStage" column="current_stage" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="auditTime" column="audit_time" />
|
||||
<result property="auditType" column="audit_type" />
|
||||
<result property="auditResult" column="audit_result" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, auditor, opinion, current_stage, create_time, audit_time, audit_type, audit_result
|
||||
from biz_meeting_audit_log
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingAuditLogResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingAuditLogResult" parameterType="BizMeetingAuditLog">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
||||
<if test="auditor != null and auditor != ''">and auditor = #{auditor}</if>
|
||||
</where>
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingAuditLog" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_audit_log
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="auditor != null and auditor != ''">auditor,</if>
|
||||
<if test="opinion != null and opinion != ''">opinion,</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="auditTime != null">audit_time,</if>
|
||||
<if test="auditType != null and auditType != ''">audit_type,</if>
|
||||
<if test="auditResult != null and auditResult != ''">audit_result,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="auditor != null and auditor != ''">#{auditor},</if>
|
||||
<if test="opinion != null and opinion != ''">#{opinion},</if>
|
||||
<if test="currentStage != null and currentStage != ''">#{currentStage},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="auditTime != null">#{auditTime},</if>
|
||||
<if test="auditType != null and auditType != ''">#{auditType},</if>
|
||||
<if test="auditResult != null and auditResult != ''">#{auditResult},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingAuditLog">
|
||||
update biz_meeting_audit_log
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="auditor != null and auditor != ''">auditor = #{auditor},</if>
|
||||
<if test="opinion != null and opinion != ''">opinion = #{opinion},</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage = #{currentStage},</if>
|
||||
<if test="createTime != null">create_time = #{createTime},</if>
|
||||
<if test="auditTime != null">audit_time = #{auditTime},</if>
|
||||
<if test="auditType != null and auditType != ''">audit_type = #{auditType},</if>
|
||||
<if test="auditResult != null and auditResult != ''">audit_result = #{auditResult},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_audit_log where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||
delete from biz_meeting_audit_log where id in
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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.business.mapper.BizMeetingExecutorMapper">
|
||||
|
||||
<resultMap type="BizMeetingExecutor" id="BizMeetingExecutorResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="assignedBy" column="assigned_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, user_id, assigned_by, create_time
|
||||
from biz_meeting_executor
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingExecutorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingExecutorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId}
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByUserId" resultMap="BizMeetingExecutorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where user_id = #{userId}
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingExecutorResult" parameterType="BizMeetingExecutor">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="userId != null">and user_id = #{userId}</if>
|
||||
</where>
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingExecutor" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_executor
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="assignedBy != null">assigned_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="assignedBy != null">#{assignedBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into biz_meeting_executor (meeting_id, user_id, assigned_by, create_time)
|
||||
values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingExecutor">
|
||||
update biz_meeting_executor
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="userId != null">user_id = #{userId},</if>
|
||||
<if test="assignedBy != null">assigned_by = #{assignedBy},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_executor where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByMeetingId" parameterType="Long">
|
||||
delete from biz_meeting_executor where meeting_id = #{meetingId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,153 @@
|
||||
<?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.business.mapper.BizMeetingInvoiceMapper">
|
||||
|
||||
<resultMap type="BizMeetingInvoice" id="BizMeetingInvoiceResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="materialId" column="material_id" />
|
||||
<result property="ossUrl" column="oss_url" />
|
||||
<result property="invoiceType" column="invoice_type" />
|
||||
<result property="amount" column="amount" />
|
||||
<result property="creatorId" column="creator_id" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="recognizeStatus" column="recognize_status" />
|
||||
<result property="errorMsg" column="error_msg" />
|
||||
<result property="sourceFilename" column="source_filename" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, material_id, oss_url, invoice_type, amount, creator_id, create_time, update_time,
|
||||
recognize_status, error_msg, source_filename
|
||||
from biz_meeting_invoice
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingInvoiceResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingInvoiceResult" parameterType="BizMeetingInvoice">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="materialId != null">and material_id = #{materialId}</if>
|
||||
<if test="invoiceType != null and invoiceType != ''">and invoice_type = #{invoiceType}</if>
|
||||
</where>
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<select id="selectByMaterialId" resultMap="BizMeetingInvoiceResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where material_id = #{materialId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingInvoiceResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId}
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingInvoice" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_invoice
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="materialId != null">material_id,</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">oss_url,</if>
|
||||
<if test="invoiceType != null and invoiceType != ''">invoice_type,</if>
|
||||
<if test="amount != null">amount,</if>
|
||||
<if test="creatorId != null">creator_id,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="recognizeStatus != null and recognizeStatus != ''">recognize_status,</if>
|
||||
<if test="errorMsg != null">error_msg,</if>
|
||||
<if test="sourceFilename != null">source_filename,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="materialId != null">#{materialId},</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">#{ossUrl},</if>
|
||||
<if test="invoiceType != null and invoiceType != ''">#{invoiceType},</if>
|
||||
<if test="amount != null">#{amount},</if>
|
||||
<if test="creatorId != null">#{creatorId},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="recognizeStatus != null and recognizeStatus != ''">#{recognizeStatus},</if>
|
||||
<if test="errorMsg != null">#{errorMsg},</if>
|
||||
<if test="sourceFilename != null">#{sourceFilename},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingInvoice">
|
||||
update biz_meeting_invoice
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id = #{meetingId},</if>
|
||||
<if test="materialId != null">material_id = #{materialId},</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">oss_url = #{ossUrl},</if>
|
||||
<if test="invoiceType != null and invoiceType != ''">invoice_type = #{invoiceType},</if>
|
||||
<if test="amount != null">amount = #{amount},</if>
|
||||
<if test="creatorId != null">creator_id = #{creatorId},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="recognizeStatus != null and recognizeStatus != ''">recognize_status = #{recognizeStatus},</if>
|
||||
<if test="errorMsg != null">error_msg = #{errorMsg},</if>
|
||||
<if test="sourceFilename != null">source_filename = #{sourceFilename},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_invoice where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||
delete from biz_meeting_invoice where id in
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByMaterialId" parameterType="Long">
|
||||
delete from biz_meeting_invoice where material_id = #{materialId}
|
||||
</delete>
|
||||
|
||||
<!-- 兜底扫描: UNRECOGNIZED 状态且 create_time 早于 N 分钟前的行 (处理程序重启 / OCR 临时挂掉导致的遗漏) -->
|
||||
<select id="selectUnrecognizedOlderThanMinutes" resultMap="BizMeetingInvoiceResult" parameterType="int">
|
||||
<include refid="selectFields"/>
|
||||
where recognize_status = 'UNRECOGNIZED'
|
||||
and create_time is not null
|
||||
and create_time < date_sub(now(), INTERVAL #{minutes} MINUTE)
|
||||
order by create_time ASC
|
||||
limit 100
|
||||
</select>
|
||||
|
||||
<!-- 单文件后台 OCR 完成: 状态+金额 一起更新 (按 material_id) -->
|
||||
<update id="updateStatusByMaterial">
|
||||
update biz_meeting_invoice
|
||||
set recognize_status = #{recognizeStatus},
|
||||
<if test="errorMsg != null">error_msg = #{errorMsg},</if>
|
||||
update_time = now()
|
||||
where material_id = #{materialId}
|
||||
and recognize_status = 'UNRECOGNIZED'
|
||||
</update>
|
||||
|
||||
<!-- 单文件后台 OCR 完成: 更新金额 (前提: 状态已是 RECOGNIZED) -->
|
||||
<update id="updateAmountByMaterial">
|
||||
update biz_meeting_invoice
|
||||
set amount = #{amount},
|
||||
update_time = now()
|
||||
where material_id = #{materialId}
|
||||
</update>
|
||||
|
||||
<!-- 兜底 OCR 用: 同时更新状态 + 金额 + 错误信息 (按主键) -->
|
||||
<update id="updateStatusAndAmountByPrimaryKey">
|
||||
update biz_meeting_invoice
|
||||
set recognize_status = #{recognizeStatus},
|
||||
amount = #{amount},
|
||||
error_msg = #{errorMsg},
|
||||
update_time = now()
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -14,10 +14,13 @@
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="orgName" column="org_name" />
|
||||
<result property="address" column="address" />
|
||||
<result property="currentStage" column="current_stage" />
|
||||
<result property="supervisionOpinion" column="supervision_opinion" />
|
||||
<result property="supervisionBy" column="supervision_by" />
|
||||
<result property="supervisionTime" column="supervision_time" />
|
||||
<result property="materialAuditStage" column="material_audit_stage" />
|
||||
<result property="voucherAuditStage" column="voucher_audit_stage" />
|
||||
<result property="invitationUrl" column="invitation_url" />
|
||||
<result property="scheduleUrl" column="schedule_url" />
|
||||
<result property="laborSigned" column="labor_signed" />
|
||||
@@ -27,7 +30,7 @@
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
select meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, current_stage, supervision_opinion, supervision_by, supervision_time, invitation_url, schedule_url, labor_signed, create_by, create_time, update_by, update_time
|
||||
select meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, voucher_audit_stage, invitation_url, schedule_url, labor_signed, create_by, create_time, update_by, update_time
|
||||
from biz_meeting
|
||||
</sql>
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingResult" parameterType="Long">
|
||||
@@ -65,10 +68,13 @@
|
||||
<if test="startTime != null">start_time,</if>
|
||||
<if test="endTime != null">end_time,</if>
|
||||
<if test="orgName != null and orgName != ''">org_name,</if>
|
||||
<if test="address != null and address != ''">address,</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage,</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion,</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by,</if>
|
||||
<if test="supervisionTime != null">supervision_time,</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage,</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">voucher_audit_stage,</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url,</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url,</if>
|
||||
<if test="laborSigned != null and laborSigned != ''">labor_signed,</if>
|
||||
@@ -86,10 +92,13 @@
|
||||
<if test="startTime != null">#{startTime},</if>
|
||||
<if test="endTime != null">#{endTime},</if>
|
||||
<if test="orgName != null and orgName != ''">#{orgName},</if>
|
||||
<if test="address != null and address != ''">#{address},</if>
|
||||
<if test="currentStage != null and currentStage != ''">#{currentStage},</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">#{supervisionOpinion},</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">#{supervisionBy},</if>
|
||||
<if test="supervisionTime != null">#{supervisionTime},</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">#{materialAuditStage},</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">#{voucherAuditStage},</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">#{invitationUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">#{scheduleUrl},</if>
|
||||
<if test="laborSigned != null and laborSigned != ''">#{laborSigned},</if>
|
||||
@@ -109,10 +118,13 @@
|
||||
<if test="startTime != null and startTime != ''">start_time = #{startTime},</if>
|
||||
<if test="endTime != null and endTime != ''">end_time = #{endTime},</if>
|
||||
<if test="orgName != null and orgName != ''">org_name = #{orgName},</if>
|
||||
<if test="address != null and address != ''">address = #{address},</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage = #{currentStage},</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion = #{supervisionOpinion},</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by = #{supervisionBy},</if>
|
||||
<if test="supervisionTime != null and supervisionTime != ''">supervision_time = #{supervisionTime},</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage = #{materialAuditStage},</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">voucher_audit_stage = #{voucherAuditStage},</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url = #{invitationUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url = #{scheduleUrl},</if>
|
||||
<if test="laborSigned != null and laborSigned != ''">labor_signed = #{laborSigned},</if>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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.business.mapper.BizMeetingMaterialMapper">
|
||||
|
||||
<resultMap type="BizMeetingMaterial" id="BizMeetingMaterialResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="materialType" column="material_type" />
|
||||
<result property="subType" column="sub_type" />
|
||||
<result property="fileName" column="file_name" />
|
||||
<result property="ossUrl" column="oss_url" />
|
||||
<result property="amount" column="amount" />
|
||||
<result property="creatorId" column="creator_id" />
|
||||
<result property="createTime" column="create_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, material_type, sub_type, file_name, oss_url, amount, creator_id, create_time
|
||||
from biz_meeting_material
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingMaterialResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingMaterialResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId}
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingMaterial" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_material
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="materialType != null and materialType != ''">material_type,</if>
|
||||
<if test="subType != null and subType != ''">sub_type,</if>
|
||||
<if test="fileName != null and fileName != ''">file_name,</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">oss_url,</if>
|
||||
<if test="amount != null">amount,</if>
|
||||
<if test="creatorId != null">creator_id,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="materialType != null and materialType != ''">#{materialType},</if>
|
||||
<if test="subType != null and subType != ''">#{subType},</if>
|
||||
<if test="fileName != null and fileName != ''">#{fileName},</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">#{ossUrl},</if>
|
||||
<if test="amount != null">#{amount},</if>
|
||||
<if test="creatorId != null">#{creatorId},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into biz_meeting_material (meeting_id, material_type, sub_type, file_name, oss_url, amount, creator_id, create_time)
|
||||
values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.meetingId}, #{item.materialType}, #{item.subType}, #{item.fileName}, #{item.ossUrl},
|
||||
#{item.amount}, #{item.creatorId}, #{item.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingMaterial">
|
||||
update biz_meeting_material
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="materialType != null and materialType != ''">material_type = #{materialType},</if>
|
||||
<if test="subType != null and subType != ''">sub_type = #{subType},</if>
|
||||
<if test="fileName != null and fileName != ''">file_name = #{fileName},</if>
|
||||
<if test="ossUrl != null and ossUrl != ''">oss_url = #{ossUrl},</if>
|
||||
<if test="amount != null">amount = #{amount},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateAmount">
|
||||
update biz_meeting_material set amount = #{amount} where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_material where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByMeetingId" parameterType="Long">
|
||||
delete from biz_meeting_material where meeting_id = #{meetingId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?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.business.mapper.BizMeetingSupervisorMapper">
|
||||
|
||||
<resultMap type="BizMeetingSupervisor" id="BizMeetingSupervisorResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="assignedBy" column="assigned_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, user_id, assigned_by, create_time
|
||||
from biz_meeting_supervisor
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingSupervisorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingSupervisorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId}
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByUserId" resultMap="BizMeetingSupervisorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where user_id = #{userId}
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingSupervisorResult" parameterType="BizMeetingSupervisor">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="userId != null">and user_id = #{userId}</if>
|
||||
</where>
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingSupervisor" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_supervisor
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="assignedBy != null">assigned_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="assignedBy != null">#{assignedBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into biz_meeting_supervisor (meeting_id, user_id, assigned_by, create_time)
|
||||
values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingSupervisor">
|
||||
update biz_meeting_supervisor
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="userId != null">user_id = #{userId},</if>
|
||||
<if test="assignedBy != null">assigned_by = #{assignedBy},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_supervisor where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByMeetingId" parameterType="Long">
|
||||
delete from biz_meeting_supervisor where meeting_id = #{meetingId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user