feat(mcp): 新增合同查询工具 contract.list/detail.query (REQ-20260911-0002) #8
@@ -335,6 +335,64 @@
|
||||
|
||||
---
|
||||
|
||||
## 12. contract.list.query
|
||||
|
||||
分页查询合同列表,对应老系统 `/contract/listByPage` 合同列表页(权限库 `webpage` id=394 `contractList`,页面字段定义 `ContractListOutput`)。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m2 库 `contract_new` 表,固定 `soft_delete = 0`、`id DESC`
|
||||
- 行级权限(`PermissionService.applyContractRowFilter`,与站点/出纸机同一 `getByIsAgent(propertyFlag=true)` 变体,agent 维度落 `contract_agent_id`,property 维度落 `property_id`):
|
||||
- 总部(isagent=0):全部;
|
||||
- 代理商(isagent=1):`contract_agent_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
|
||||
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
|
||||
- 富化(按页批量 IN 查询,消除 N+1):`contractAgentName`/`customerAgentName` 查 m0 `agent`,`createName` 查 m0 `user`;`saleName`/`propertyName`/`customerName`/`customerNo` 为合同表冗余快照,直接返回。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| contractNo | string | 否 | 合同编号,模糊匹配 |
|
||||
| contractName | string | 否 | 合同名称,模糊匹配 |
|
||||
| propertyName | string | 否 | 物业名称,模糊匹配 |
|
||||
| customerName | string | 否 | 客户名称(金蝶客户名称快照),模糊匹配 |
|
||||
| customerNo | string | 否 | 客户编号(=物业 serialnumber),精确匹配 |
|
||||
| contractAgentId | number | 否 | 合同所属公司 id,精确匹配 |
|
||||
| customerAgentId | number | 否 | 客户所属公司 id,精确匹配 |
|
||||
| propertyId | number | 否 | 所属物业 id,精确匹配 |
|
||||
| contractStatus | number | 否 | 合同状态:0=待审 1=驳回 2=正常 3=过期 4=终止 |
|
||||
| expireRemind | number | 否 | 到期提醒开关:0=关 1=开 |
|
||||
| endDateFrom | string | 否 | 合同结束日期起,格式 yyyy-MM-dd(查即将到期合同用) |
|
||||
| endDateTo | string | 否 | 合同结束日期止,格式 yyyy-MM-dd(含当天) |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
|
||||
|
||||
**出参 records 字段**:`contract_new` 主要字段(id、contractNo、contractName、contractAgentId/customerAgentId、propertyId/propertyName、customerNo/customerName、amount、contact、startDate/endDate(`yyyy-MM-dd`)、expireRemind/remindDays/remindTime/reminded、saleId/saleName、contractStatus、stopReasonType/stopReason、createId、createTime)+ 富化字段 `contractAgentName`/`customerAgentName`/`createName` + 状态文案 `contractStatusText`。
|
||||
|
||||
---
|
||||
|
||||
## 13. contract.detail.query
|
||||
|
||||
按 id 或 contractNo 查询合同详情(二选一必填),行级权限与 `contract.list.query` 一致。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m2 库 `contract_new` + `contract_file`(附件)+ `contract_process`(流程/待办/操作记录)
|
||||
- 合同不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露合同是否存在);id/contractNo 都未传时 notice 提示"必须传一个"。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 条件必填 | 合同 id;与 contractNo 二选一 |
|
||||
| contractNo | string | 条件必填 | 合同编号,精确匹配;与 id 二选一 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:与列表同一套字段(含富化和 `contractStatusText`)。
|
||||
- `files`:合同附件列表,`id`/`fileName`/`fileUrl`/`createTime`,按 id 正序。
|
||||
- `processes`:审批流程记录,按 id 正序,字段 `nodeType`(0=创建 1=部门内审核 2=层级审核 3=终止 4=抄送)、`auditLevel`、`operationType`(0=创建 1=审核通过 2=审核驳回 3=终止 4=修改再提交 5=抄送)、`userId`/`userName`、`scheduleState`(0=无需待办 1=待办 2=已办)、`remark`、`operateTime`,均附 `nodeTypeText`/`operationTypeText`/`scheduleStateText` 文案。
|
||||
|
||||
---
|
||||
|
||||
## 附录:在线查看工具清单
|
||||
|
||||
```bash
|
||||
|
||||
@@ -16,9 +16,10 @@ WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'operation');
|
||||
|
||||
-- 作用域-工具:sys.ping / agent.count / agent.list.query / property.list.query / agent.detail.query / property.detail.query
|
||||
-- / mall.order.list.query / mall.order.detail.query / mall.order.logistics.query
|
||||
-- / contract.list.query / contract.detail.query
|
||||
INSERT INTO scope_tool (scope_id, tool_id, create_id)
|
||||
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
|
||||
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query', 'mall.order.list.query', 'mall.order.detail.query', 'mall.order.logistics.query')
|
||||
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query', 'mall.order.list.query', 'mall.order.detail.query', 'mall.order.logistics.query', 'contract.list.query', 'contract.detail.query')
|
||||
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
|
||||
|
||||
-- 作用域:hardware(出纸机查询)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同附件
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_file")
|
||||
public class ContractFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同ID(0=上传后未提交的孤儿附件,定时清理)
|
||||
*/
|
||||
private Long contractId;
|
||||
|
||||
/**
|
||||
* 原始文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件访问地址
|
||||
*/
|
||||
private String fileUrl;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同主表
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_new")
|
||||
public class ContractNew implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同编号(properyt.serialnumber + "C" + 三位流水,如452542C001)
|
||||
*/
|
||||
private String contractNo;
|
||||
|
||||
/**
|
||||
* 合同名称
|
||||
*/
|
||||
private String contractName;
|
||||
|
||||
/**
|
||||
* 合同所属公司(=物业所属销售所属公司,审批流按此公司匹配)
|
||||
*/
|
||||
private Long contractAgentId;
|
||||
|
||||
/**
|
||||
* 客户所属公司(=创建人提交时选择的所属公司)
|
||||
*/
|
||||
private Long customerAgentId;
|
||||
|
||||
/**
|
||||
* 所属物业ID(关联权限库properyt)
|
||||
*/
|
||||
private Long propertyId;
|
||||
|
||||
/**
|
||||
* 物业名称(冗余快照)
|
||||
*/
|
||||
private String propertyName;
|
||||
|
||||
/**
|
||||
* 客户编号快照(=properyt.serialnumber,编号流水/按客户归集用)
|
||||
*/
|
||||
private String customerNo;
|
||||
|
||||
/**
|
||||
* 客户名称快照(=properyt.jinide_name 金蝶客户名称)
|
||||
*/
|
||||
private String customerName;
|
||||
|
||||
/**
|
||||
* 合同金额(元)
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
private String contact;
|
||||
|
||||
/**
|
||||
* 合同开始日期
|
||||
*/
|
||||
private Date startDate;
|
||||
|
||||
/**
|
||||
* 合同结束日期
|
||||
*/
|
||||
private Date endDate;
|
||||
|
||||
/**
|
||||
* 到期提醒开关0:关;1:开
|
||||
*/
|
||||
private Integer expireRemind;
|
||||
|
||||
/**
|
||||
* 到期提醒天数
|
||||
*/
|
||||
private Integer remindDays;
|
||||
|
||||
/**
|
||||
* 到期提醒时间(=end_date-remind_days 当天09:30:00)
|
||||
*/
|
||||
private Date remindTime;
|
||||
|
||||
/**
|
||||
* 到期提醒是否已发送0:未发送;1:已发送
|
||||
*/
|
||||
private Integer reminded;
|
||||
|
||||
/**
|
||||
* 所属销售ID(=物业所属销售)
|
||||
*/
|
||||
private Long saleId;
|
||||
|
||||
/**
|
||||
* 所属销售姓名(冗余快照)
|
||||
*/
|
||||
private String saleName;
|
||||
|
||||
/**
|
||||
* 合同状态0:待审;1:驳回;2:正常;3:过期;4:终止
|
||||
*/
|
||||
private Integer contractStatus;
|
||||
|
||||
/**
|
||||
* 终止原因分类(cancel_reason.content快照,type=6)
|
||||
*/
|
||||
private String stopReasonType;
|
||||
|
||||
/**
|
||||
* 终止详细原因
|
||||
*/
|
||||
private String stopReason;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同流程/待办/操作记录表
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_process")
|
||||
public class ContractProcess implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同ID
|
||||
*/
|
||||
private Long contractId;
|
||||
|
||||
/**
|
||||
* 合同编号(冗余)
|
||||
*/
|
||||
private String contractNo;
|
||||
|
||||
/**
|
||||
* 节点类型0:创建;1:部门内审核;2:层级审核;3:终止;4:抄送
|
||||
*/
|
||||
private Integer nodeType;
|
||||
|
||||
/**
|
||||
* 审核层级(层级审核为第几级,其余0)
|
||||
*/
|
||||
private Integer auditLevel;
|
||||
|
||||
/**
|
||||
* 操作类型0:创建;1:审核通过;2:审核驳回;3:终止;4:修改再提交;5:抄送
|
||||
*/
|
||||
private Integer operationType;
|
||||
|
||||
/**
|
||||
* 待办人/操作人ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 待办人/操作人姓名
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 待办状态0:无需待办;1:待办;2:已办
|
||||
*/
|
||||
private Integer scheduleState;
|
||||
|
||||
/**
|
||||
* 备注/驳回原因/终止原因/抄送文案
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 办理时间(待办未办为NULL)
|
||||
*/
|
||||
private Date operateTime;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractFile;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同附件 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractFileMapper extends BaseMapper<ContractFile> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同主表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractNewMapper extends BaseMapper<ContractNew> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractProcess;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同流程/待办/操作记录 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractProcessMapper extends BaseMapper<ContractProcess> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.ContractFile;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.ContractProcess;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractFileMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractNewMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractProcessMapper;
|
||||
import me.iiv.agent.mcp.ToolObject;
|
||||
import me.iiv.agent.mcp.XqTool;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 合同域工具:contract.list.query / contract.detail.query(对应老系统 /contract/listByPage
|
||||
* 合同列表页 web_id=394,数据在 m2 库 contract_new/contract_file/contract_process,
|
||||
* 行权限走 PermissionService.applyContractRowFilter)。
|
||||
*/
|
||||
@Component
|
||||
public class ContractTools implements ToolObject {
|
||||
|
||||
private static final int DEFAULT_ROWS = 20;
|
||||
private static final int MAX_ROWS = 100;
|
||||
|
||||
private static final Map<Integer, String> CONTRACT_STATUS_TEXT = Map.of(
|
||||
0, "待审", 1, "驳回", 2, "正常", 3, "过期", 4, "终止");
|
||||
private static final Map<Integer, String> NODE_TYPE_TEXT = Map.of(
|
||||
0, "创建", 1, "部门内审核", 2, "层级审核", 3, "终止", 4, "抄送");
|
||||
private static final Map<Integer, String> OPERATION_TYPE_TEXT = Map.of(
|
||||
0, "创建", 1, "审核通过", 2, "审核驳回", 3, "终止", 4, "修改再提交", 5, "抄送");
|
||||
private static final Map<Integer, String> SCHEDULE_STATE_TEXT = Map.of(
|
||||
0, "无需待办", 1, "待办", 2, "已办");
|
||||
|
||||
final ContractNewMapper contractNewMapper;
|
||||
final ContractFileMapper contractFileMapper;
|
||||
final ContractProcessMapper contractProcessMapper;
|
||||
final AgentMapper agentMapper;
|
||||
final UserMapper userMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public ContractTools(ContractNewMapper contractNewMapper, ContractFileMapper contractFileMapper,
|
||||
ContractProcessMapper contractProcessMapper, AgentMapper agentMapper,
|
||||
UserMapper userMapper, PermissionService permissionService) {
|
||||
this.contractNewMapper = contractNewMapper;
|
||||
this.contractFileMapper = contractFileMapper;
|
||||
this.contractProcessMapper = contractProcessMapper;
|
||||
this.agentMapper = agentMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@XqTool(category = "customer", kind = "read")
|
||||
@Tool(name = "contract.list.query", description = "查询合同列表(分页),对应老系统 /contract/listByPage 合同列表页,"
|
||||
+ "数据来源 m2 库 contract_new 表。"
|
||||
+ "行级权限:代理商身份(权限组 isagent=1)只能看到 自己+直属下级代理+被授权代管代理 作为合同所属公司的合同,"
|
||||
+ "以及被授权代管物业名下的合同;销售部门非负责人只能看到 自己创建的物业 及 被授权代管代理名下物业 的合同;"
|
||||
+ "总部身份(isagent=0)可看全部。"
|
||||
+ "分页:page 从 1 开始,rows 默认 20、上限 100,超出自动截断为 100 并在返回的 notice 字段提示。"
|
||||
+ "contractStatus 含义:0=待审 1=驳回 2=正常 3=过期 4=终止;expireRemind 含义:0=关 1=开。"
|
||||
+ "返回 total(符合条件的总数)、records(本页记录,按 id 倒序,含 contractStatusText 状态文案)。")
|
||||
public Map<String, Object> contractListQuery(
|
||||
@ToolParam(required = false, description = "合同编号,模糊匹配") String contractNo,
|
||||
@ToolParam(required = false, description = "合同名称,模糊匹配") String contractName,
|
||||
@ToolParam(required = false, description = "物业名称,模糊匹配") String propertyName,
|
||||
@ToolParam(required = false, description = "客户名称(金蝶客户名称快照),模糊匹配") String customerName,
|
||||
@ToolParam(required = false, description = "客户编号(=物业 serialnumber),精确匹配") String customerNo,
|
||||
@ToolParam(required = false, description = "合同所属公司 id,精确匹配") Long contractAgentId,
|
||||
@ToolParam(required = false, description = "客户所属公司 id,精确匹配") Long customerAgentId,
|
||||
@ToolParam(required = false, description = "所属物业 id,精确匹配") Long propertyId,
|
||||
@ToolParam(required = false, description = "合同状态:0=待审 1=驳回 2=正常 3=过期 4=终止") Integer contractStatus,
|
||||
@ToolParam(required = false, description = "到期提醒开关:0=关 1=开") Integer expireRemind,
|
||||
@ToolParam(required = false, description = "合同结束日期起,格式 yyyy-MM-dd(查即将到期合同用)") String endDateFrom,
|
||||
@ToolParam(required = false, description = "合同结束日期止,格式 yyyy-MM-dd(含当天)") String endDateTo,
|
||||
@ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page,
|
||||
@ToolParam(required = false, description = "每页条数,默认 20,上限 100(超出截断为 100)") Integer rows) {
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
int pageNo = page == null || page < 1 ? 1 : page;
|
||||
boolean truncated = rows != null && rows > MAX_ROWS;
|
||||
int pageSize = rows == null || rows < 1 ? DEFAULT_ROWS : Math.min(rows, MAX_ROWS);
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<ContractNew>()
|
||||
.eq(ContractNew::getSoftDelete, 0);
|
||||
if (StringUtils.hasText(contractNo)) {
|
||||
wrapper.like(ContractNew::getContractNo, contractNo);
|
||||
}
|
||||
if (StringUtils.hasText(contractName)) {
|
||||
wrapper.like(ContractNew::getContractName, contractName);
|
||||
}
|
||||
if (StringUtils.hasText(propertyName)) {
|
||||
wrapper.like(ContractNew::getPropertyName, propertyName);
|
||||
}
|
||||
if (StringUtils.hasText(customerName)) {
|
||||
wrapper.like(ContractNew::getCustomerName, customerName);
|
||||
}
|
||||
if (StringUtils.hasText(customerNo)) {
|
||||
wrapper.eq(ContractNew::getCustomerNo, customerNo);
|
||||
}
|
||||
if (contractAgentId != null) {
|
||||
wrapper.eq(ContractNew::getContractAgentId, contractAgentId);
|
||||
}
|
||||
if (customerAgentId != null) {
|
||||
wrapper.eq(ContractNew::getCustomerAgentId, customerAgentId);
|
||||
}
|
||||
if (propertyId != null) {
|
||||
wrapper.eq(ContractNew::getPropertyId, propertyId);
|
||||
}
|
||||
if (contractStatus != null) {
|
||||
wrapper.eq(ContractNew::getContractStatus, contractStatus);
|
||||
}
|
||||
if (expireRemind != null) {
|
||||
wrapper.eq(ContractNew::getExpireRemind, expireRemind);
|
||||
}
|
||||
if (StringUtils.hasText(endDateFrom)) {
|
||||
wrapper.ge(ContractNew::getEndDate, endDateFrom);
|
||||
}
|
||||
if (StringUtils.hasText(endDateTo)) {
|
||||
wrapper.le(ContractNew::getEndDate, endDateTo);
|
||||
}
|
||||
// 行级权限(getByIsAgent propertyFlag 变体,统一走 PermissionService)
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
Long total = contractNewMapper.selectCount(wrapper);
|
||||
wrapper.orderByDesc(ContractNew::getId)
|
||||
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
|
||||
List<ContractNew> contracts = contractNewMapper.selectList(wrapper);
|
||||
|
||||
// 合同所属公司 / 客户所属公司 / 创建人 名称批量富化(一次 IN 查询,消除 N+1)
|
||||
List<Long> agentIds = contracts.stream()
|
||||
.flatMap(c -> Stream.of(c.getContractAgentId(), c.getCustomerAgentId()))
|
||||
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
Map<Long, String> agentNames = agentIds.isEmpty()
|
||||
? Map.of()
|
||||
: agentMapper.selectBatchIds(agentIds).stream()
|
||||
.collect(Collectors.toMap(Agent::getId, Agent::getName, (a, b) -> a));
|
||||
List<Long> creatorIds = contracts.stream().map(ContractNew::getCreateId)
|
||||
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
Map<Long, String> creatorNames = creatorIds.isEmpty()
|
||||
? Map.of()
|
||||
: userMapper.selectBatchIds(creatorIds).stream()
|
||||
.collect(Collectors.toMap(User::getId, User::getName, (a, b) -> a));
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
|
||||
List<Map<String, Object>> records = new ArrayList<>();
|
||||
for (ContractNew contract : contracts) {
|
||||
records.add(toRecord(contract, agentNames, creatorNames, sdf, df));
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("total", total);
|
||||
result.put("page", pageNo);
|
||||
result.put("rows", pageSize);
|
||||
if (truncated) {
|
||||
result.put("notice", "rows 超过上限 100,已截断为 100");
|
||||
}
|
||||
result.put("records", records);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同详情:行权限与 contract.list.query 一致 + 精确 id/contractNo 叠加。
|
||||
* files 来自 m2.contract_file,processes 来自 m2.contract_process(按 id 正序)。
|
||||
*/
|
||||
@XqTool(category = "customer", kind = "read")
|
||||
@Tool(name = "contract.detail.query", description = "按 id 或 contractNo 查询合同详情(二选一必填),数据来源 m2 库 contract_new 表。"
|
||||
+ "行级权限与 contract.list.query 一致;合同不存在或当前身份无权查看时 record 为 null 并在 notice 字段说明(两者不区分)。"
|
||||
+ "返回 record(合同全字段,含终止原因和状态文案 contractStatusText)、"
|
||||
+ "files(合同附件列表,含 fileName/fileUrl)、"
|
||||
+ "processes(审批流程/待办/操作记录,按 id 正序;nodeType 含义:0=创建 1=部门内审核 2=层级审核 3=终止 4=抄送;"
|
||||
+ "operationType 含义:0=创建 1=审核通过 2=审核驳回 3=终止 4=修改再提交 5=抄送;"
|
||||
+ "scheduleState 含义:0=无需待办 1=待办 2=已办,均附对应文案字段)。")
|
||||
public Map<String, Object> contractDetailQuery(
|
||||
@ToolParam(required = false, description = "合同 id;与 contractNo 二选一必填") Long id,
|
||||
@ToolParam(required = false, description = "合同编号,精确匹配;与 id 二选一必填") String contractNo) {
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (id == null && !StringUtils.hasText(contractNo)) {
|
||||
result.put("record", null);
|
||||
result.put("files", List.of());
|
||||
result.put("processes", List.of());
|
||||
result.put("notice", "id 和 contractNo 必须传一个");
|
||||
return result;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<ContractNew>()
|
||||
.eq(ContractNew::getSoftDelete, 0);
|
||||
if (id != null) {
|
||||
wrapper.eq(ContractNew::getId, id);
|
||||
} else {
|
||||
wrapper.eq(ContractNew::getContractNo, contractNo);
|
||||
}
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
ContractNew contract = contractNewMapper.selectOne(wrapper);
|
||||
|
||||
if (contract == null) {
|
||||
result.put("record", null);
|
||||
result.put("files", List.of());
|
||||
result.put("processes", List.of());
|
||||
result.put("notice", (id != null ? "id=" + id : "contractNo=" + contractNo) + " 不存在或当前身份无权查看");
|
||||
return result;
|
||||
}
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Map<Long, String> agentNames = agentNamesOf(contract);
|
||||
String createName = null;
|
||||
if (contract.getCreateId() != null) {
|
||||
User creator = userMapper.selectById(contract.getCreateId());
|
||||
createName = creator == null ? null : creator.getName();
|
||||
}
|
||||
result.put("record", toRecord(contract, agentNames,
|
||||
Map.of(contract.getCreateId() == null ? 0L : contract.getCreateId(),
|
||||
createName == null ? "未查询到" : createName),
|
||||
sdf, df));
|
||||
|
||||
List<ContractFile> files = contractFileMapper.selectList(new LambdaQueryWrapper<ContractFile>()
|
||||
.eq(ContractFile::getSoftDelete, 0)
|
||||
.eq(ContractFile::getContractId, contract.getId())
|
||||
.orderByAsc(ContractFile::getId));
|
||||
List<Map<String, Object>> fileList = new ArrayList<>();
|
||||
for (ContractFile file : files) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", file.getId());
|
||||
item.put("fileName", file.getFileName());
|
||||
item.put("fileUrl", file.getFileUrl());
|
||||
item.put("createTime", file.getCreateTime() == null ? null : sdf.format(file.getCreateTime()));
|
||||
fileList.add(item);
|
||||
}
|
||||
result.put("files", fileList);
|
||||
|
||||
List<ContractProcess> processes = contractProcessMapper.selectList(new LambdaQueryWrapper<ContractProcess>()
|
||||
.eq(ContractProcess::getSoftDelete, 0)
|
||||
.eq(ContractProcess::getContractId, contract.getId())
|
||||
.orderByAsc(ContractProcess::getId));
|
||||
List<Map<String, Object>> processList = new ArrayList<>();
|
||||
for (ContractProcess process : processes) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", process.getId());
|
||||
item.put("nodeType", process.getNodeType());
|
||||
item.put("nodeTypeText", NODE_TYPE_TEXT.getOrDefault(process.getNodeType(), "未知"));
|
||||
item.put("auditLevel", process.getAuditLevel());
|
||||
item.put("operationType", process.getOperationType());
|
||||
item.put("operationTypeText", OPERATION_TYPE_TEXT.getOrDefault(process.getOperationType(), "未知"));
|
||||
item.put("userId", process.getUserId());
|
||||
item.put("userName", process.getUserName());
|
||||
item.put("scheduleState", process.getScheduleState());
|
||||
item.put("scheduleStateText", SCHEDULE_STATE_TEXT.getOrDefault(process.getScheduleState(), "未知"));
|
||||
item.put("remark", process.getRemark());
|
||||
item.put("operateTime", process.getOperateTime() == null ? null : sdf.format(process.getOperateTime()));
|
||||
processList.add(item);
|
||||
}
|
||||
result.put("processes", processList);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> toRecord(ContractNew contract, Map<Long, String> agentNames,
|
||||
Map<Long, String> creatorNames,
|
||||
SimpleDateFormat sdf, SimpleDateFormat df) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", contract.getId());
|
||||
record.put("contractNo", contract.getContractNo());
|
||||
record.put("contractName", contract.getContractName());
|
||||
record.put("contractAgentId", contract.getContractAgentId());
|
||||
record.put("contractAgentName", contract.getContractAgentId() == null
|
||||
? "未查询到" : agentNames.getOrDefault(contract.getContractAgentId(), "未查询到"));
|
||||
record.put("customerAgentId", contract.getCustomerAgentId());
|
||||
record.put("customerAgentName", contract.getCustomerAgentId() == null
|
||||
? "未查询到" : agentNames.getOrDefault(contract.getCustomerAgentId(), "未查询到"));
|
||||
record.put("propertyId", contract.getPropertyId());
|
||||
record.put("propertyName", contract.getPropertyName());
|
||||
record.put("customerNo", contract.getCustomerNo());
|
||||
record.put("customerName", contract.getCustomerName());
|
||||
record.put("amount", contract.getAmount());
|
||||
record.put("contact", contract.getContact());
|
||||
record.put("startDate", contract.getStartDate() == null ? null : df.format(contract.getStartDate()));
|
||||
record.put("endDate", contract.getEndDate() == null ? null : df.format(contract.getEndDate()));
|
||||
record.put("expireRemind", contract.getExpireRemind());
|
||||
record.put("remindDays", contract.getRemindDays());
|
||||
record.put("remindTime", contract.getRemindTime() == null ? null : sdf.format(contract.getRemindTime()));
|
||||
record.put("reminded", contract.getReminded());
|
||||
record.put("saleId", contract.getSaleId());
|
||||
record.put("saleName", contract.getSaleName());
|
||||
record.put("contractStatus", contract.getContractStatus());
|
||||
record.put("contractStatusText", CONTRACT_STATUS_TEXT.getOrDefault(contract.getContractStatus(), "未知"));
|
||||
record.put("stopReasonType", contract.getStopReasonType());
|
||||
record.put("stopReason", contract.getStopReason());
|
||||
record.put("createId", contract.getCreateId());
|
||||
record.put("createName", contract.getCreateId() == null
|
||||
? "未查询到" : creatorNames.getOrDefault(contract.getCreateId(), "未查询到"));
|
||||
record.put("createTime", contract.getCreateTime() == null ? null : sdf.format(contract.getCreateTime()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<Long, String> agentNamesOf(ContractNew contract) {
|
||||
List<Long> agentIds = Stream.of(contract.getContractAgentId(), contract.getCustomerAgentId())
|
||||
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (agentIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return agentMapper.selectBatchIds(agentIds).stream()
|
||||
.collect(Collectors.toMap(Agent::getId, Agent::getName, (a, b) -> a));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import me.iiv.agent.entity.m0.HardwareView;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
@@ -400,4 +401,28 @@ public class PermissionService {
|
||||
wrapper.in(HardwareView::getAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同(m2 contract_new)行权限:与站点/出纸机同一 getByIsAgent 变体
|
||||
* (propertyFlag=true),agent 维度落到 contract_agent_id(合同所属公司,
|
||||
* =物业所属销售所属公司),property 维度落到 property_id。
|
||||
* 老系统 /contract/listByPage 源码不在本工作区,按合同表字段语义对齐
|
||||
* district/hardware 的过滤组合(2026-09-11 决策)。
|
||||
*/
|
||||
public void applyContractRowFilter(LambdaQueryWrapper<ContractNew> wrapper, User user) {
|
||||
RowScope scope = resolveRowScope(user);
|
||||
if (scope.headquarters) {
|
||||
return;
|
||||
}
|
||||
if (scope.salesPropertyIds != null) {
|
||||
wrapper.in(ContractNew::getPropertyId, scope.salesPropertyIds);
|
||||
return;
|
||||
}
|
||||
if (!scope.managedPropertyIds.isEmpty()) {
|
||||
wrapper.and(w -> w.in(ContractNew::getContractAgentId, scope.ids)
|
||||
.or().in(ContractNew::getPropertyId, scope.managedPropertyIds));
|
||||
} else {
|
||||
wrapper.in(ContractNew::getContractAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.ContractFile;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.ContractProcess;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractFileMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractNewMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractProcessMapper;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContext;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContractToolsTest {
|
||||
|
||||
@Mock
|
||||
ContractNewMapper contractNewMapper;
|
||||
@Mock
|
||||
ContractFileMapper contractFileMapper;
|
||||
@Mock
|
||||
ContractProcessMapper contractProcessMapper;
|
||||
@Mock
|
||||
AgentMapper agentMapper;
|
||||
@Mock
|
||||
UserMapper userMapper;
|
||||
@Mock
|
||||
PermissionService permissionService;
|
||||
|
||||
ContractTools tools;
|
||||
User user;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
ContractNew.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
ContractFile.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
ContractProcess.class);
|
||||
tools = new ContractTools(contractNewMapper, contractFileMapper, contractProcessMapper,
|
||||
agentMapper, userMapper, permissionService);
|
||||
user = new User();
|
||||
user.setId(7L);
|
||||
AuthContextHolder.set(new AuthContext(null, user));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AuthContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void listBuildsLegacyFiltersAndTruncatesRows() {
|
||||
ContractNew contract = sampleContract();
|
||||
when(contractNewMapper.selectCount(any())).thenReturn(1L);
|
||||
when(contractNewMapper.selectList(any())).thenReturn(List.of(contract));
|
||||
Agent contractAgent = new Agent();
|
||||
contractAgent.setId(17L);
|
||||
contractAgent.setName("合同所属公司A");
|
||||
Agent customerAgent = new Agent();
|
||||
customerAgent.setId(18L);
|
||||
customerAgent.setName("客户所属公司B");
|
||||
when(agentMapper.selectBatchIds(anyList())).thenReturn(List.of(contractAgent, customerAgent));
|
||||
User creator = new User();
|
||||
creator.setId(1000L);
|
||||
creator.setName("张三");
|
||||
when(userMapper.selectBatchIds(anyList())).thenReturn(List.of(creator));
|
||||
|
||||
Map<String, Object> result = tools.contractListQuery(
|
||||
"452542C", "服务合同", "望京", "金蝶客户", "452542",
|
||||
17L, 18L, 1L, 2, 1, "2026-01-01", "2026-12-31", 1, 500);
|
||||
|
||||
assertEquals(1, result.get("page"));
|
||||
assertEquals(100, result.get("rows"));
|
||||
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
|
||||
|
||||
ArgumentCaptor<LambdaQueryWrapper<ContractNew>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(contractNewMapper).selectList(captor.capture());
|
||||
String sql = captor.getValue().getSqlSegment();
|
||||
assertTrue(sql.contains("contract_no LIKE"));
|
||||
assertTrue(sql.contains("contract_name LIKE"));
|
||||
assertTrue(sql.contains("property_name LIKE"));
|
||||
assertTrue(sql.contains("customer_name LIKE"));
|
||||
assertTrue(sql.contains("customer_no ="));
|
||||
assertTrue(sql.contains("contract_agent_id ="));
|
||||
assertTrue(sql.contains("customer_agent_id ="));
|
||||
assertTrue(sql.contains("property_id ="));
|
||||
assertTrue(sql.contains("contract_status ="));
|
||||
assertTrue(sql.contains("expire_remind ="));
|
||||
assertTrue(sql.contains("end_date >="));
|
||||
assertTrue(sql.contains("end_date <="));
|
||||
assertTrue(sql.contains("soft_delete ="));
|
||||
assertTrue(sql.contains("ORDER BY id DESC"));
|
||||
verify(permissionService).applyContractRowFilter(any(), any());
|
||||
|
||||
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
|
||||
assertEquals("452542C001", record.get("contractNo"));
|
||||
assertEquals("合同所属公司A", record.get("contractAgentName"));
|
||||
assertEquals("客户所属公司B", record.get("customerAgentName"));
|
||||
assertEquals("张三", record.get("createName"));
|
||||
assertEquals(2, record.get("contractStatus"));
|
||||
assertEquals("正常", record.get("contractStatusText"));
|
||||
assertEquals(new BigDecimal("120000.00"), record.get("amount"));
|
||||
assertEquals("2026-01-01", record.get("startDate"));
|
||||
assertEquals("2026-12-31", record.get("endDate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailRequiresIdOrContractNo() {
|
||||
Map<String, Object> result = tools.contractDetailQuery(null, " ");
|
||||
|
||||
assertNull(result.get("record"));
|
||||
assertEquals("id 和 contractNo 必须传一个", result.get("notice"));
|
||||
verifyNoInteractions(contractNewMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsNullRecordWithNoticeWhenMissingOrUnauthorized() {
|
||||
when(contractNewMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
Map<String, Object> result = tools.contractDetailQuery(404L, null);
|
||||
|
||||
assertNull(result.get("record"));
|
||||
assertEquals("id=404 不存在或当前身份无权查看", result.get("notice"));
|
||||
verify(permissionService).applyContractRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void detailByContractNoReturnsRecordFilesAndProcesses() {
|
||||
ContractNew contract = sampleContract();
|
||||
when(contractNewMapper.selectOne(any())).thenReturn(contract);
|
||||
Agent contractAgent = new Agent();
|
||||
contractAgent.setId(17L);
|
||||
contractAgent.setName("合同所属公司A");
|
||||
when(agentMapper.selectBatchIds(anyList())).thenReturn(List.of(contractAgent));
|
||||
User creator = new User();
|
||||
creator.setId(1000L);
|
||||
creator.setName("张三");
|
||||
when(userMapper.selectById(1000L)).thenReturn(creator);
|
||||
|
||||
ContractFile file = new ContractFile();
|
||||
file.setId(1L);
|
||||
file.setContractId(9L);
|
||||
file.setFileName("合同扫描件.pdf");
|
||||
file.setFileUrl("https://oss.example.com/contract/9/scan.pdf");
|
||||
when(contractFileMapper.selectList(any())).thenReturn(List.of(file));
|
||||
|
||||
ContractProcess process = new ContractProcess();
|
||||
process.setId(1L);
|
||||
process.setContractId(9L);
|
||||
process.setNodeType(1);
|
||||
process.setAuditLevel(0);
|
||||
process.setOperationType(1);
|
||||
process.setUserId(2000L);
|
||||
process.setUserName("李四");
|
||||
process.setScheduleState(2);
|
||||
process.setRemark("同意");
|
||||
when(contractProcessMapper.selectList(any())).thenReturn(List.of(process));
|
||||
|
||||
Map<String, Object> result = tools.contractDetailQuery(null, "452542C001");
|
||||
|
||||
ArgumentCaptor<LambdaQueryWrapper<ContractNew>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(contractNewMapper).selectOne(captor.capture());
|
||||
assertTrue(captor.getValue().getSqlSegment().contains("contract_no ="));
|
||||
verify(permissionService).applyContractRowFilter(any(), any());
|
||||
|
||||
Map<String, Object> record = (Map<String, Object>) result.get("record");
|
||||
assertEquals("452542C001", record.get("contractNo"));
|
||||
assertEquals("合同所属公司A", record.get("contractAgentName"));
|
||||
assertEquals("未查询到", record.get("customerAgentName"));
|
||||
assertEquals("张三", record.get("createName"));
|
||||
assertEquals("正常", record.get("contractStatusText"));
|
||||
|
||||
List<Map<String, Object>> files = (List<Map<String, Object>>) result.get("files");
|
||||
assertEquals(1, files.size());
|
||||
assertEquals("合同扫描件.pdf", files.get(0).get("fileName"));
|
||||
assertEquals("https://oss.example.com/contract/9/scan.pdf", files.get(0).get("fileUrl"));
|
||||
|
||||
List<Map<String, Object>> processes = (List<Map<String, Object>>) result.get("processes");
|
||||
assertEquals(1, processes.size());
|
||||
assertEquals("部门内审核", processes.get(0).get("nodeTypeText"));
|
||||
assertEquals("审核通过", processes.get(0).get("operationTypeText"));
|
||||
assertEquals("已办", processes.get(0).get("scheduleStateText"));
|
||||
assertEquals("李四", processes.get(0).get("userName"));
|
||||
assertEquals("同意", processes.get(0).get("remark"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void springAiPublishesBothReviewedToolSchemas() {
|
||||
var callbacks = org.springframework.ai.tool.method.MethodToolCallbackProvider.builder()
|
||||
.toolObjects(tools).build().getToolCallbacks();
|
||||
Map<String, String> schemas = Arrays.stream(callbacks).collect(Collectors.toMap(
|
||||
callback -> callback.getToolDefinition().name(),
|
||||
callback -> callback.getToolDefinition().inputSchema()));
|
||||
|
||||
assertEquals(2, schemas.size());
|
||||
assertTrue(schemas.containsKey("contract.list.query"));
|
||||
assertTrue(schemas.containsKey("contract.detail.query"));
|
||||
assertTrue(schemas.get("contract.list.query").contains("contractStatus"));
|
||||
assertTrue(schemas.get("contract.detail.query").contains("contractNo"));
|
||||
}
|
||||
|
||||
private static ContractNew sampleContract() {
|
||||
ContractNew contract = new ContractNew();
|
||||
contract.setId(9L);
|
||||
contract.setContractNo("452542C001");
|
||||
contract.setContractName("望京SOHO厕纸服务合同");
|
||||
contract.setContractAgentId(17L);
|
||||
contract.setCustomerAgentId(18L);
|
||||
contract.setPropertyId(1L);
|
||||
contract.setPropertyName("望京SOHO");
|
||||
contract.setCustomerNo("452542");
|
||||
contract.setCustomerName("金蝶客户名称");
|
||||
contract.setAmount(new BigDecimal("120000.00"));
|
||||
contract.setContact("王五");
|
||||
contract.setStartDate(java.sql.Date.valueOf("2026-01-01"));
|
||||
contract.setEndDate(java.sql.Date.valueOf("2026-12-31"));
|
||||
contract.setExpireRemind(1);
|
||||
contract.setRemindDays(30);
|
||||
contract.setSaleId(1001L);
|
||||
contract.setSaleName("销售甲");
|
||||
contract.setContractStatus(2);
|
||||
contract.setCreateId(1000L);
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package me.iiv.agent.permission;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.AgentManageDistrictAuth;
|
||||
import me.iiv.agent.entity.m0.Authority;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.AuthorityMapper;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseAgentAuthMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/** PermissionService.applyContractRowFilter 直接单测:contract_new 行级权限各分支(与站点/出纸机同一 getByIsAgent 变体)。 */
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PermissionServiceContractRowFilterTest {
|
||||
|
||||
@Mock
|
||||
AuthorityMapper authorityMapper;
|
||||
@Mock
|
||||
AgentMapper agentMapper;
|
||||
@Mock
|
||||
WarehouseAgentAuthMapper warehouseAgentAuthMapper;
|
||||
@Mock
|
||||
AgentManageDistrictAuthMapper agentManageDistrictAuthMapper;
|
||||
@Mock
|
||||
ProperytMapper properytMapper;
|
||||
@Mock
|
||||
StringRedisTemplate redisTemplate;
|
||||
@Mock
|
||||
HashOperations<String, Object, Object> hashOperations;
|
||||
|
||||
PermissionService permissionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
ContractNew.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
Properyt.class);
|
||||
permissionService = new PermissionService(authorityMapper, agentMapper,
|
||||
warehouseAgentAuthMapper, agentManageDistrictAuthMapper, properytMapper, redisTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void headquartersContractFilterAddsNoBoundary() {
|
||||
User user = user(46L, 1L, null);
|
||||
when(authorityMapper.selectById(1L)).thenReturn(authority(1L, 0));
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
assertTrue(wrapper.getSqlSegment().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentContractFilterUsesSelfSubordinateAndManagedAgents() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
Agent subordinate = new Agent();
|
||||
subordinate.setId(2L);
|
||||
when(agentMapper.selectList(any())).thenReturn(List.of(subordinate));
|
||||
WarehouseAgentAuth managedAuth = new WarehouseAgentAuth();
|
||||
managedAuth.setManagerAgentId(3L);
|
||||
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managedAuth));
|
||||
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
Collection<Object> values = wrapper.getParamNameValuePairs().values();
|
||||
assertTrue(sql.contains("contract_agent_id IN"));
|
||||
assertFalse(sql.contains("property_id"));
|
||||
assertTrue(values.contains(1L));
|
||||
assertTrue(values.contains(2L));
|
||||
assertTrue(values.contains(3L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentContractFilterAddsManagedPropertyOrBranch() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
when(agentMapper.selectList(any())).thenReturn(List.of());
|
||||
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
|
||||
AgentManageDistrictAuth districtAuth = new AgentManageDistrictAuth();
|
||||
districtAuth.setPropertyId(44L);
|
||||
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(districtAuth));
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
Collection<Object> values = wrapper.getParamNameValuePairs().values();
|
||||
assertTrue(sql.contains("contract_agent_id IN"));
|
||||
assertTrue(sql.contains("property_id IN"));
|
||||
assertTrue(sql.contains("OR"));
|
||||
assertTrue(values.contains(1L));
|
||||
assertTrue(values.contains(44L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void salesNonLeaderContractFilterUsesOwnCreatedPropertyIds() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
user.setDeptId(5L);
|
||||
user.setPostId(7L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
when(agentMapper.selectList(any())).thenReturn(List.of());
|
||||
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
|
||||
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
|
||||
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
|
||||
Properyt property = new Properyt();
|
||||
property.setId(55L);
|
||||
when(properytMapper.selectList(any())).thenReturn(List.of(property));
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
assertTrue(sql.contains("property_id IN"));
|
||||
assertFalse(sql.contains("contract_agent_id"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(55L));
|
||||
verifyNoInteractions(agentManageDistrictAuthMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void salesNonLeaderWithoutVisiblePropertiesFiltersToZero() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
user.setDeptId(5L);
|
||||
user.setPostId(7L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
when(agentMapper.selectList(any())).thenReturn(List.of());
|
||||
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
|
||||
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
|
||||
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
|
||||
when(properytMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
LambdaQueryWrapper<ContractNew> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyContractRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
assertTrue(sql.contains("property_id IN"));
|
||||
assertFalse(sql.contains("contract_agent_id"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(0L));
|
||||
verifyNoInteractions(agentManageDistrictAuthMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentUserWithoutAgentIdIsDeniedForContractFilter() {
|
||||
User user = user(1000L, 108L, null);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> permissionService.applyContractRowFilter(new LambdaQueryWrapper<>(), user));
|
||||
}
|
||||
|
||||
private static Authority authority(Long id, int isAgent) {
|
||||
Authority authority = new Authority();
|
||||
authority.setId(id);
|
||||
authority.setIsagent(isAgent);
|
||||
authority.setSoftDelete(0);
|
||||
return authority;
|
||||
}
|
||||
|
||||
private static User user(Long id, Long authorityId, Long agentId) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setAuthorityId(authorityId);
|
||||
user.setAgentId(agentId);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user