18 KiB
合同管理 — 02 业务逻辑(列表查询)
步骤 2 产出 | 日期:2026-05-31
权威源码:CrmContractServiceImpl.buildQueryWrapper+queryPageList;前端views/crm/contract/index.vue
关联 API:04-api-spec.mdC01(GET /crm/contract/list)
1. 调用链
index.vue getList()
→ listContract(queryParams) // GET /crm/contract/list
→ CrmContractController.list(bo, pageQuery)
→ CrmContractServiceImpl.queryPageList(bo, pageQuery)
① buildQueryWrapper(bo) // 组装 WHERE
② selectVoPage // 分页查 crm_contract
③ 逐行 setCanApprove // 审批权限(非 WHERE 条件)
→ 返回 { rows, total }
Python 重写时必须拆成两层:
_build_contract_filters(...)— 仅翻译buildQueryWrapper的 SQL 条件list_contracts(...)— 分页查询后,对每条记录调用can_user_approve(contract_id, current_user_id)
2. 列表 Tab 与 Query 参数映射
前端 handleViewChange 在切换 Tab 时重置下列字段,再调用 getList()。
getList() 还会根据是否显式选了 status 设置 excludeStatus(见 §4)。
Tab activeView |
ownerUserId |
pendingApprovalUserId |
approvedByUserId |
status |
说明 |
|---|---|---|---|---|---|
my 我的合同 |
当前用户 | — | — | — | 负责人=我 |
all 全部合同 |
— | — | — | — | 不限负责人 |
submitted 待审批 |
— | 当前用户 | — | — | 见 §5.1 |
approved 已审批 |
— | — | 当前用户 | — | 见 §5.2 |
executing 执行中 |
— | — | — | executing |
合同状态精确匹配 |
finished 已完成 |
— | — | — | finished |
|
invalid 已作废 |
— | — | — | invalid |
此时不再 exclude invalid |
切换 Tab 时均清空:approvalStatus(筛选表单项,与 Tab 无关时可单独选)。
flowchart LR
subgraph tabs [Tab activeView]
my[my]
all[all]
sub[submitted]
app[approved]
exe[executing]
fin[finished]
inv[invalid]
end
subgraph params [queryParams 写入]
O[ownerUserId=当前用户]
P[pendingApprovalUserId=当前用户]
A[approvedByUserId=当前用户]
S1[status=executing/finished/invalid]
end
my --> O
sub --> P
app --> A
exe --> S1
fin --> S1
inv --> S1
3. buildQueryWrapper 条件逐项说明
Java 方法位置:CrmContractServiceImpl 第 76–152 行。
所有业务表查询默认带 租户隔离(Entity 继承 Tenant)+ del_flag=0(@TableLogic,Wrapper 自动追加)。
排序(固定): ORDER BY create_time DESC
3.1 文本搜索:联合 OR vs 分别 AND
| 场景 | 条件 | SQL 语义 |
|---|---|---|
| 联合搜索 | contractNo、contractName、customerName 三者非空且值完全相同 |
(contract_no LIKE ? OR contract_name LIKE ? OR customer_name LIKE ?) |
| 普通筛选 | 否则 | 各字段独立 LIKE(有值才加) |
前端触发联合搜索:
- 工具栏 快速检索框
handleQuickSearch:把同一关键字写入三个字段queryParams.contractNo = queryParams.contractName = queryParams.customerName = quickSearch - 签订日期 快捷按钮(今日/昨日/明日/本周)在改
dateRange后会调用handleQuickSearch(),若快捷框有字则同样走 OR 逻辑
Python 实现要点:
if (
contract_no and contract_name and customer_name
and contract_no == contract_name == customer_name
):
kw = contract_no
q = q.filter(or_(
CrmContract.contract_no.like(f"%{kw}%"),
CrmContract.contract_name.like(f"%{kw}%"),
CrmContract.customer_name.like(f"%{kw}%"),
))
else:
# 分别 like
3.2 精确匹配字段
| Bo 字段 | 列 | 条件 |
|---|---|---|
customerId |
customer_id |
= |
opportunityId |
opportunity_id |
= |
quoteId |
quote_id |
= |
contractType |
contract_type |
= |
status |
status |
= |
approvalStatus |
approval_status |
= |
ownerUserId |
owner_user_id |
= |
3.3 排除状态 excludeStatus
lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmContract::getStatus, bo.getExcludeStatus());
| 值 | SQL |
|---|---|
excludeStatus = 'invalid' |
status <> 'invalid' |
前端规则(getList):
- 若
status未设置 → 传excludeStatus = 'invalid'(默认隐藏作废合同) - 若
status已设置(含 Tab「已作废」或筛选项)→ 不传excludeStatus
因此:my / all / submitted / approved Tab 下默认都看不到作废合同;只有 invalid Tab 或用户手动选状态=作废时才显示。
3.4 金额区间
| Bo 来源 | 列 | 条件 |
|---|---|---|
params.minAmount |
amount |
>= |
params.maxAmount |
amount |
<= |
RuoYi 将部分 Query 参数放在 BaseEntity.params Map;前端 ContractQuery 若传 minAmount/maxAmount,Python 需从 body/query 同等读取。
3.5 签订日期 signDateBegin / signDateEnd
| 字段 | 列 | 条件 |
|---|---|---|
signDateBegin |
sign_date |
>= LocalDate.parse(begin) |
signDateEnd |
sign_date |
<= LocalDate.parse(end) |
前端 dateRange 在 getList 里写入这两个字段;快捷日期与 筛选区日期 共用 dateRange。
3.6 创建时间 beginTime / endTime
lqw.between(beginTime != null && endTime != null, create_time, beginTime, endTime);
两端都有值才加 BETWEEN;列表页默认不传。
4. 审批相关子查询(Tab 核心)
4.1 待审批 Tab — pendingApprovalUserId
前端: submitted Tab → pendingApprovalUserId = 当前登录用户 id
Java:
contract.id IN (
SELECT DISTINCT contract_id FROM crm_contract_approval
WHERE approver_id = :pendingApprovalUserId
AND approval_result = 'pending'
AND is_current = 1
AND del_flag = 0
)
语义:
- 仅看 当前审批节点(
is_current = 1)且 轮到当前用户 的合同 - 合同本身
status通常为approving,但 Wrapper 不再额外过滤 status(靠审批表约束)
Python 安全写法: 用 SQLAlchemy in_(select(...)) 或 exists(),禁止 Java 式字符串拼接 approver_id。
4.2 已审批 Tab — approvedByUserId
前端: approved Tab → approvedByUserId = 当前登录用户 id
Java(两步,非子查询):
- 查
crm_contract_approval:approver_id = ?ANDapproval_result IN ('approved','rejected') - 取 distinct
contract_id→crm_contract.id IN (...) - 若第 1 步无记录 →
id = -1(保证空结果,避免 IN 空集语义差异)
语义:
- 当前用户 曾经审批过(通过或驳回)的合同都会列出
- 不要求
is_current = 1 - 不过滤合同当前
status(可能已 executing / finished 等)
4.3 Tab 与审批字段互斥
切换 Tab 时前端会 显式清空 其它 Tab 用的 id 字段(见 §2 表),避免 ownerUserId 与 pendingApprovalUserId 叠加导致结果为空。
5. 列表结果后处理 — canApprove
buildQueryWrapper 不包含审批权限;在 queryPageList 查完分页后:
vo.setCanApprove(contractApprovalService.canUserApprove(vo.getId(), currentUserId));
canUserApprove(contractId, userId) 规则
- 查该合同全部审批记录,按
step升序 - 找到
approver_id == userId的记录;找不到 →false - 该记录
approval_result必须为pending - 前置步骤(step 更小)的记录的
approval_result必须全部为approved - 全部满足 →
true(列表/详情展示「审批」按钮)
与 Tab「待审批」的区别:
| 待审批 Tab 列表 | canApprove |
|
|---|---|---|
| 用途 | 筛出「待我处理」的合同集合 | 单行是否显示审批按钮 |
| 条件 | is_current=1 + pending |
前置步骤已通过 + 本人 pending |
| 典型 | 合同在 submitted Tab 出现 | 多步审批中,仅当前步骤审批人为 true |
6. 列表页统计卡片(非 API)
index.vue 的 loadStatistics() 不调用 GET /statistics/overview,而是对 当前页 contractList 聚合:
| 指标 | 计算 |
|---|---|
| 总数 | total(分页 total,非当前页 rows 长度) |
| 总金额 | Σ amount / 10000(万元) |
| 已回款 | Σ receivedAmount / 10000 |
| 回款进度 | 已回款 / 总金额 × 100% |
移植注意: 后端 C17 统计接口与列表 Tab 统计 无关;列表页 Fresh Mint 改造时保留前端聚合逻辑即可。
7. 详情查询补充(queryById,列表抽屉用)
列表点击行打开抽屉时调用 GET /crm/contract/{id},在 buildQueryWrapper 之外还有:
| 步骤 | 说明 |
|---|---|
canApprove |
同 §5 |
supplierName |
有 supplierId 时查 crm_supplier_info |
products |
contractProductService.queryListByContractId |
框架合同 priceTiers |
contract_type=framework 时加载 crm_contract_product_price_tier 挂到各产品 |
列表 展开行产品 若仅用列表 Vo 字段,可能不含 tiers;详情抽屉走 C02 全量数据。
8. Python 重写检查清单(C01)
翻译 buildQueryWrapper 时逐项对照:
- 租户
tenant_no+del_flag=0 - 联合 OR / 分别 LIKE(§3.1)
excludeStatus与前端getList联动(§3.3)- Tab 五类用户 id / status 字段(§2)
pendingApprovalUserId子查询含is_current=1(§4.1)approvedByUserId无记录时返回空列表(§4.2)signDateBegin/End、min/maxAmount、beginTime/endTime(§3.4–3.6)ORDER BY create_time DESC- 分页后逐行
canApprove(§5) - 参数化查询,不用 Java
inSql字符串拼接
9. 与状态机章节的衔接
列表查询 只读 status / 审批表,不修改状态。写入类操作的状态约束见 §10。
10. 合同状态机与操作约束(步骤 3)
10.1 双字段:status 与 approval_status
数据库 crm_contract(sx.sql):
| 字段 | 字典/取值 | 含义 |
|---|---|---|
status |
draft 草稿 · approving 审批中 · approved 已审批 · executing 执行中 · finished 已完成 · invalid 已作废 |
主业务状态,驱动列表 Tab、按钮可见性 |
approval_status |
NULL · pending · approved · rejected |
审批结论;草稿时为 NULL |
约定(Java 注释):
- 新增/编辑草稿:
status=draft,approval_status必须 NULL(勿留 pending) - 提交审批:
status=approving,approval_status=pending - 审批全部通过:
status=approved,approval_status=approved - 审批驳回:
status=draft,approval_status=rejected(可改稿后再次提交) - 撤回审批:
status=draft,approval_status置 NULL(需用 UpdateWrapper 显式写 null)
前端 index.vue 按钮示例:
- 「开始执行」:
status === 'approved' && approvalStatus === 'approved' - 「作废」:
status∈draft | approving | approved | executing - 「编辑/删除」:仅
draft
10.2 主状态流转图
stateDiagram-v2
[*] --> draft: insertByBo / 撤回 / 审批驳回
draft --> approving: submitApproval
approving --> draft: withdrawApproval
approving --> approved: 末步审批通过
approved --> executing: startExecute 或 signContract
executing --> finished: finishContract
draft --> invalid: invalidContract
approving --> invalid: invalidContract
approved --> invalid: invalidContract
executing --> invalid: invalidContract
finished --> [*]: 不可作废
invalid --> [*]
进入 executing 的两条路径(均要求当前 status=approved):
| 接口 | 额外写入 |
|---|---|
PUT /startExecute |
仅 status=executing |
PUT /sign |
status=executing + actualSignDate + contractFile + performanceStatus=normal + performanceRate=0 |
二者互斥使用场景:列表/ExecuteDialog 走 startExecute;form 签订走 signContract。
10.3 各操作前置条件与副作用
| 操作 | API | 前置 status |
合同字段变更 | 审批表 / 其它 |
|---|---|---|---|---|
| 新增 | POST / |
— | 默认 draft,approval_status=NULL,生成编号/名称,金额字段初始化 |
— |
| 修改 | PUT / |
draft | 可改业务字段;草稿保持 approval_status=NULL;签供方/日期变则 重生成编号名称 |
产品明细 batch 更新 |
| 删除 | DELETE /{ids} |
draft(逐条校验) | 逻辑删除 del_flag=1 |
— |
| 提交审批 | PUT /submitApproval |
draft | → approving + approval_status=pending |
按 approverIds 插入 N 条 approval;step=1..N,is_current 仅首步=1;框架合同走 §10.5 校验 |
| 批量提交 | PUT /batchSubmitApproval |
同上 | 同上 | 逐 id 调 submitApproval,失败记日志继续 |
| 撤回 | PUT /withdrawApproval |
approving | → draft,approval_status=NULL |
物理删除该合同下 approval_result=pending 的记录 |
| 审批通过(逐步) | POST /approval 等 |
合同通常 approving |
末步通过 → approved+approved;否则不动 status |
当前记录 → approved,is_current=0;下一步 pending → is_current=1 |
| 审批驳回 | 同上 | — | → draft + rejected |
当前记录 → rejected |
| 开始执行 | PUT /startExecute |
approved | → executing |
— |
| 签订 | PUT /sign |
approved | → executing + 签订信息 + 履约字段 |
TODO:回款计划激活(Java 未实现) |
| 完成 | PUT /finish |
executing | → finished;可写 remark |
— |
| 作废 | PUT /invalid |
非 finished | → invalid + invalidReason + invalidTime + remark |
— |
| 续签 | POST /renew/{parentId} |
原合同任意态可读 | 新建子合同 draft,contractNature=renewal,parentContractId,renewalCount+1 |
复制产品等 |
| 批量分配负责人 | PUT /batch/assign |
无 status 限制 | 更新 ownerUserId / ownerUserName |
— |
| 上传附件/模板 | POST 若干 | 存在即可 | 更新文件/模板字段 | 不改变 status |
错误文案(Python 应对齐):
- 「只有草稿状态的合同才能修改 / 提交审批 / 删除」
- 「只有审批中的合同才能撤回」
- 「只有已审批的合同才能开始执行」
- 「只有执行中的合同才能完成」
- 「已完成的合同不能作废」
- 「作废原因不能为空」
10.4 审批流与主状态的联动(CrmContractApprovalServiceImpl.approveContract)
- 校验:当前用户=审批人、记录=pending、
canUserApprove=true - 写审批记录:
approval_result、approval_opinion、approval_time、is_current=0 - 若 approved:
- 存在更大
step且仍为 pending 的下一条 → 将其is_current=1 - 否则(全部通过)→ 合同
status=approved,approval_status=approved
- 存在更大
- 若 rejected: → 合同
status=draft,approval_status=rejected(不会设为 invalid)
列表页 approveContract(id, comment) 走 A08(按合同 id 找 is_current=1 且 pending 的本人记录,再调 approveContract(approvalId, …))。
10.5 提交审批专项校验 — 框架合同
validateFrameworkContractForSubmitApproval(仅 contract_type=framework):
- 至少一条产品明细
- 每个产品至少一条
crm_contract_product_price_tier - 每条 tier:
startDate/endDate合法、单价>0、必须含调价函 ossId
非框架合同:走通用产品/金额校验(validateContractTypeAndProductsPresence 等),在 insert/update 时已执行。
10.6 删除 vs 作废
删除 deleteWithValidByIds |
作废 invalidContract |
|
|---|---|---|
| 允许状态 | 仅 draft | draft / approving / approved / executing |
| ���止 | 非 draft | finished |
| 数据 | MyBatis 逻辑删 | 保留行,status=invalid |
| 列表 | 默认 exclude invalid | 「已作废」Tab status=invalid |
10.7 创建类操作的初始状态
| 入口 | 初始 status |
说明 |
|---|---|---|
insertByBo |
draft |
默认负责人=当前用户 |
renewContract |
draft(新合同) | 不修改原合同状态 |
createFromQuote / createFromOpportunity |
走 insertByBo → draft |
报价单状态改为 converted(quote 侧) |
10.8 Python 重写检查清单(状态相关 API)
实现 C03–C13、C25–C26、A08 时逐项对照:
- 草稿
approval_status为 NULL(insert/update/withdraw) - 撤回时删除 pending 审批行,不是 soft delete
- 驳回回到 draft+rejected,不是 invalid
- 多步审批:中间步通过不改合同 status,只推进
is_current - 末步通过才
approved/approved startExecute与signContract均要求status=approved- 作废写
invalidReason、invalidTime - 删除仅 draft + 软删
- 框架合同 submit 前 tier 校验(§10.5)
- 事务边界与 Java
@Transactional一致(审批+改合同同事务)
变更记录
| 日期 | 说明 |
|---|---|
| 2026-05-31 | 步骤 2:buildQueryWrapper + Tab + 联合搜索 + 审批子查询 + canApprove |
| 2026-05-31 | 步骤 3:status/approval_status 双字段、状态机、操作约束、审批联动 |
| 2026-05-31 | 步骤 6:跨模块金额回调见 05-cross-module-calls.md |