diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md
index 8a167c8..e31f98a 100644
--- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md
+++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md
@@ -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
diff --git a/xiaoqu-agent-service/sql/seed-dev.sql b/xiaoqu-agent-service/sql/seed-dev.sql
index c2d915b..b6ba167 100644
--- a/xiaoqu-agent-service/sql/seed-dev.sql
+++ b/xiaoqu-agent-service/sql/seed-dev.sql
@@ -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(出纸机查询)
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractFile.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractFile.java
new file mode 100644
index 0000000..773c556
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractFile.java
@@ -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;
+
+/**
+ *
+ * 合同附件
+ *
+ *
+ * @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;
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java
new file mode 100644
index 0000000..a567150
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java
@@ -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;
+
+/**
+ *
+ * 合同主表
+ *
+ *
+ * @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;
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractProcess.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractProcess.java
new file mode 100644
index 0000000..fbac222
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractProcess.java
@@ -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;
+
+/**
+ *
+ * 合同流程/待办/操作记录表
+ *
+ *
+ * @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;
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractFileMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractFileMapper.java
new file mode 100644
index 0000000..da89728
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractFileMapper.java
@@ -0,0 +1,16 @@
+package me.iiv.agent.mapper.m2;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import me.iiv.agent.entity.m2.ContractFile;
+
+/**
+ *
+ * 合同附件 Mapper 接口
+ *
+ *
+ * @author codegen
+ * @since 2026-09-11
+ */
+public interface ContractFileMapper extends BaseMapper {
+
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractNewMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractNewMapper.java
new file mode 100644
index 0000000..839411d
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractNewMapper.java
@@ -0,0 +1,16 @@
+package me.iiv.agent.mapper.m2;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import me.iiv.agent.entity.m2.ContractNew;
+
+/**
+ *
+ * 合同主表 Mapper 接口
+ *
+ *
+ * @author codegen
+ * @since 2026-09-11
+ */
+public interface ContractNewMapper extends BaseMapper {
+
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractProcessMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractProcessMapper.java
new file mode 100644
index 0000000..8b1afdd
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/ContractProcessMapper.java
@@ -0,0 +1,16 @@
+package me.iiv.agent.mapper.m2;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import me.iiv.agent.entity.m2.ContractProcess;
+
+/**
+ *
+ * 合同流程/待办/操作记录 Mapper 接口
+ *
+ *
+ * @author codegen
+ * @since 2026-09-11
+ */
+public interface ContractProcessMapper extends BaseMapper {
+
+}
diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ContractTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ContractTools.java
new file mode 100644
index 0000000..2b93e42
--- /dev/null
+++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ContractTools.java
@@ -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 CONTRACT_STATUS_TEXT = Map.of(
+ 0, "待审", 1, "驳回", 2, "正常", 3, "过期", 4, "终止");
+ private static final Map NODE_TYPE_TEXT = Map.of(
+ 0, "创建", 1, "部门内审核", 2, "层级审核", 3, "终止", 4, "抄送");
+ private static final Map OPERATION_TYPE_TEXT = Map.of(
+ 0, "创建", 1, "审核通过", 2, "审核驳回", 3, "终止", 4, "修改再提交", 5, "抄送");
+ private static final Map 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 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 wrapper = new LambdaQueryWrapper()
+ .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 contracts = contractNewMapper.selectList(wrapper);
+
+ // 合同所属公司 / 客户所属公司 / 创建人 名称批量富化(一次 IN 查询,消除 N+1)
+ List agentIds = contracts.stream()
+ .flatMap(c -> Stream.of(c.getContractAgentId(), c.getCustomerAgentId()))
+ .filter(Objects::nonNull).distinct().collect(Collectors.toList());
+ Map agentNames = agentIds.isEmpty()
+ ? Map.of()
+ : agentMapper.selectBatchIds(agentIds).stream()
+ .collect(Collectors.toMap(Agent::getId, Agent::getName, (a, b) -> a));
+ List creatorIds = contracts.stream().map(ContractNew::getCreateId)
+ .filter(Objects::nonNull).distinct().collect(Collectors.toList());
+ Map 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