feat(mcp): 回款目标/回款明细 changes 流工具(P2 应收数据准入)
- repayment-targets.changes.list:m1.saler_repayment 月度回款目标,status 0/1→INCOMPLETE/COMPLETE 未知值 fail-closed - repayment-details.changes.list:m1.saler_repayment_customer 客户×订单×周期明细,overdue_status/overdue_time 等 SaaS 既有口径只读透传 - 金额 Double 元统一转 *_minor(分,Long),四舍五入 - 行级权限:目标(总部全量/销售本人/代理范围),明细与合同同 ACL(销售房产范围/主管代理范围) - 测试:SyncEntitiesTest +5、ChangesToolsTest 注册数 4→6 并补路由验证,共 18 项全过
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 销售回款目标表(按月汇总,m1.saler_repayment)。
|
||||
* 金额列为 legacy Double(元),公开契约层统一换算为分(*_minor)。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("saler_repayment")
|
||||
public class SalerRepayment 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;
|
||||
|
||||
/** 数据状态 */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 下单公司(代理ID) */
|
||||
private Long agentId;
|
||||
|
||||
/** 销售id */
|
||||
private Long userId;
|
||||
|
||||
/** 下单公司名称 */
|
||||
private String agentName;
|
||||
|
||||
/** 销售名称 */
|
||||
private String userName;
|
||||
|
||||
/** 周期(yyyy-mm) */
|
||||
private String period;
|
||||
|
||||
/** 待收款金额(按期) */
|
||||
private Double pendingReceivableAmount;
|
||||
|
||||
/** 超期待收款金额 */
|
||||
private Double overdueReceivableAmount;
|
||||
|
||||
/** 待收款总金额 */
|
||||
private Double totalReceivableAmount;
|
||||
|
||||
/** 按期还款金额 */
|
||||
private Double pendingRepaymentAmount;
|
||||
|
||||
/** 超期还款金额 */
|
||||
private Double overdueRepaymentAmount;
|
||||
|
||||
/** 还款总金额 */
|
||||
private Double totalRepaymentAmount;
|
||||
|
||||
/** 按期目标金额 */
|
||||
private Double pendingTargetAmount;
|
||||
|
||||
/** 超期目标金额 */
|
||||
private Double overdueTargetAmount;
|
||||
|
||||
/** 目标金额 */
|
||||
private Double totalTargetAmount;
|
||||
|
||||
/** 订单数 */
|
||||
private Integer totalOrderCount;
|
||||
|
||||
/** 超期订单数 */
|
||||
private Integer overdueOrderCount;
|
||||
|
||||
/** 订单总金额 */
|
||||
private Double totalOrderAmount;
|
||||
|
||||
/** 客户数 */
|
||||
private Integer totalCustomerCount;
|
||||
|
||||
/** 超期客户数 */
|
||||
private Integer overdueCustomerCount;
|
||||
|
||||
/** 当期开票金额 */
|
||||
private Double invoiceAmount;
|
||||
|
||||
/** 完成状态(0:未完成;1:已完成) */
|
||||
private Integer status;
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 销售回款客户明细表(客户 × 订单 × 周期,m1.saler_repayment_customer)。
|
||||
* 金额列为 legacy Double(元),公开契约层统一换算为分(*_minor)。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("saler_repayment_customer")
|
||||
public class SalerRepaymentCustomer 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;
|
||||
|
||||
/** 数据状态 */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 公司id */
|
||||
private Long agentId;
|
||||
|
||||
/** 下单id */
|
||||
private Long userId;
|
||||
|
||||
/** 销售id */
|
||||
private Long salerUserId;
|
||||
|
||||
/** 预下单人员id */
|
||||
private Long placingUserId;
|
||||
|
||||
/** 公司名称 */
|
||||
private String agentName;
|
||||
|
||||
/** 下单人员名称 */
|
||||
private String userName;
|
||||
|
||||
/** 销售名称 */
|
||||
private String salerUserName;
|
||||
|
||||
/** 预下单人员名称 */
|
||||
private String placingUserName;
|
||||
|
||||
/** 周期(yyyy-mm) */
|
||||
private String period;
|
||||
|
||||
/** 待收款金额 */
|
||||
private Double pendingReceivableAmount;
|
||||
|
||||
/** 超期待收款金额 */
|
||||
private Double overdueReceivableAmount;
|
||||
|
||||
/** 待收款总金额 */
|
||||
private Double totalReceivableAmount;
|
||||
|
||||
/** 按期还款金额 */
|
||||
private Double pendingRepaymentAmount;
|
||||
|
||||
/** 超期还款金额 */
|
||||
private Double overdueRepaymentAmount;
|
||||
|
||||
/** 还款总金额 */
|
||||
private Double totalRepaymentAmount;
|
||||
|
||||
/** 订单数 */
|
||||
private Integer totalOrderCount;
|
||||
|
||||
/** 超期订单数 */
|
||||
private Integer overdueOrderCount;
|
||||
|
||||
/** 订单总金额 */
|
||||
private Double totalOrderAmount;
|
||||
|
||||
/** 当期开票金额 */
|
||||
private Double invoiceAmount;
|
||||
|
||||
/** 物业ID */
|
||||
private Long propertyId;
|
||||
|
||||
/** 站点ID */
|
||||
private Long districtId;
|
||||
|
||||
/** 物业名称 */
|
||||
private String propertyName;
|
||||
|
||||
/** 站点名称 */
|
||||
private String districtName;
|
||||
|
||||
/** 销售公司id */
|
||||
private Long salerAgentId;
|
||||
|
||||
/** 销售公司名称 */
|
||||
private String salerAgentName;
|
||||
|
||||
/** 上级id */
|
||||
private Long parentId;
|
||||
|
||||
/** 订单编号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 账单编号 */
|
||||
private String billNo;
|
||||
|
||||
/** 当期的超期状态(0:未超期;1:已超期) */
|
||||
private Integer overdueStatus;
|
||||
|
||||
/** 超期日期 */
|
||||
private Date overdueTime;
|
||||
|
||||
/** 账期类型 */
|
||||
private String billType;
|
||||
|
||||
/** 发票名称 */
|
||||
private String invoiceName;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 下单时间 */
|
||||
private Date orderTime;
|
||||
|
||||
/** 支付时间 */
|
||||
private Date payTime;
|
||||
|
||||
/** 订单开票状态(legacy 整型编码,取值域待源侧确认,契约层原样透传) */
|
||||
private Integer orderInvoiceState;
|
||||
|
||||
/** 账单周期 */
|
||||
private String billPeriod;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.SalerRepaymentCustomer;
|
||||
|
||||
/**
|
||||
* 销售回款客户明细 Mapper 接口
|
||||
*/
|
||||
public interface SalerRepaymentCustomerMapper extends BaseMapper<SalerRepaymentCustomer> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.SalerRepayment;
|
||||
|
||||
/**
|
||||
* 销售回款目标 Mapper 接口
|
||||
*/
|
||||
public interface SalerRepaymentMapper extends BaseMapper<SalerRepayment> {
|
||||
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package me.iiv.agent.mcp.sync;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.SalerRepaymentCustomer;
|
||||
import me.iiv.agent.mapper.m1.SalerRepaymentCustomerMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* repayment-details 同步实体(m1.saler_repayment_customer 客户 × 订单 × 周期回款明细)。
|
||||
* 字段口径(2026-10-16 字段核对):owner_user_id → saler_user_id(销售,稳定 user.id);
|
||||
* overdue_status/overdue_time/bill_type 为 SaaS 既有账期口径,只读透传,本侧不重算;
|
||||
* 金额列为 legacy Double(元),契约层统一转分(HALF_UP);version 由 modify_time 派生;
|
||||
* order_invoice_state 取值域未确认,按整型原样透传(不造枚举)。
|
||||
* 行级权限:Token 绑定人时走 PermissionService.applyRepaymentCustomerRowFilter
|
||||
* (与合同同一 ACL 语义:property 维度 + 代管)。
|
||||
*/
|
||||
@Component
|
||||
public class RepaymentDetailsSyncEntity extends AbstractSyncEntity implements SyncEntity {
|
||||
|
||||
public static final String NAME = "repayment-details";
|
||||
|
||||
final SalerRepaymentCustomerMapper salerRepaymentCustomerMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public RepaymentDetailsSyncEntity(SalerRepaymentCustomerMapper salerRepaymentCustomerMapper,
|
||||
PermissionService permissionService) {
|
||||
this.salerRepaymentCustomerMapper = salerRepaymentCustomerMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SyncRow> fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId,
|
||||
int limit, User user) {
|
||||
LambdaQueryWrapper<SalerRepaymentCustomer> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyRepaymentCustomerRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> rows = new ArrayList<>();
|
||||
for (SalerRepaymentCustomer row : salerRepaymentCustomerMapper.selectList(wrapper)) {
|
||||
long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime());
|
||||
rows.add(new SyncRow(toItem(row, mt), mt, row.getId()));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private Map<String, Object> toItem(SalerRepaymentCustomer row, long mt) {
|
||||
boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1;
|
||||
Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime();
|
||||
return SyncChangeSchemas.toMap(new SyncChangeSchemas.RepaymentDetailChange(
|
||||
stableId(row.getId()), stableId(row.getSalerUserId()), row.getSalerUserName(),
|
||||
stableId(row.getSalerAgentId()), row.getSalerAgentName(),
|
||||
stableId(row.getPropertyId()), row.getPropertyName(),
|
||||
stableId(row.getDistrictId()), row.getDistrictName(), row.getCustomerName(),
|
||||
row.getPeriod(), row.getBillPeriod(), row.getBillType(),
|
||||
row.getOrderNo(), row.getBillNo(),
|
||||
publicOverdueStatus(row.getOverdueStatus()), ChangesEngine.iso(row.getOverdueTime()),
|
||||
toMinor(row.getPendingReceivableAmount()), toMinor(row.getOverdueReceivableAmount()),
|
||||
toMinor(row.getTotalReceivableAmount()),
|
||||
toMinor(row.getPendingRepaymentAmount()), toMinor(row.getOverdueRepaymentAmount()),
|
||||
toMinor(row.getTotalRepaymentAmount()), toMinor(row.getInvoiceAmount()),
|
||||
row.getOrderInvoiceState(),
|
||||
ChangesEngine.iso(row.getOrderTime()), ChangesEngine.iso(row.getPayTime()),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
/** legacy Double(元)→ 分;null 原样返回 null。 */
|
||||
static Long toMinor(Double amount) {
|
||||
return amount == null ? null
|
||||
: BigDecimal.valueOf(amount).movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue();
|
||||
}
|
||||
|
||||
/** Legacy overdue flag to the stable public enum. */
|
||||
static String publicOverdueStatus(Integer status) {
|
||||
if (status == null) {
|
||||
throw invalidOverdueStatus(null);
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "NOT_OVERDUE";
|
||||
case 1 -> "OVERDUE";
|
||||
default -> throw invalidOverdueStatus(status);
|
||||
};
|
||||
}
|
||||
|
||||
private static ToolCallException invalidOverdueStatus(Integer status) {
|
||||
return new ToolCallException(McpErrorCode.DATA_INTEGRITY_ERROR,
|
||||
"回款明细超期状态无法映射到公开契约: " + status);
|
||||
}
|
||||
|
||||
private static String stableId(Long id) {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package me.iiv.agent.mcp.sync;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.SalerRepayment;
|
||||
import me.iiv.agent.mapper.m1.SalerRepaymentMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* repayment-targets 同步实体(m1.saler_repayment 销售月度回款目标)。
|
||||
* 字段口径(2026-10-16 字段核对):owner_user_id → user_id(销售,稳定 user.id);
|
||||
* 金额列为 legacy Double(元),契约层统一转分(HALF_UP);version 由 modify_time 派生;
|
||||
* status 0/1 映射 INCOMPLETE/COMPLETE,未知编码 fail-closed 拒绝(与 contracts 同一铁律)。
|
||||
* 行级权限:Token 绑定人时走 PermissionService.applyRepaymentTargetRowFilter。
|
||||
*/
|
||||
@Component
|
||||
public class RepaymentTargetsSyncEntity extends AbstractSyncEntity implements SyncEntity {
|
||||
|
||||
public static final String NAME = "repayment-targets";
|
||||
|
||||
final SalerRepaymentMapper salerRepaymentMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public RepaymentTargetsSyncEntity(SalerRepaymentMapper salerRepaymentMapper,
|
||||
PermissionService permissionService) {
|
||||
this.salerRepaymentMapper = salerRepaymentMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SyncRow> fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId,
|
||||
int limit, User user) {
|
||||
LambdaQueryWrapper<SalerRepayment> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyRepaymentTargetRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> rows = new ArrayList<>();
|
||||
for (SalerRepayment row : salerRepaymentMapper.selectList(wrapper)) {
|
||||
long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime());
|
||||
rows.add(new SyncRow(toItem(row, mt), mt, row.getId()));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private Map<String, Object> toItem(SalerRepayment row, long mt) {
|
||||
boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1;
|
||||
Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime();
|
||||
return SyncChangeSchemas.toMap(new SyncChangeSchemas.RepaymentTargetChange(
|
||||
stableId(row.getId()), stableId(row.getUserId()), row.getUserName(),
|
||||
stableId(row.getAgentId()), row.getAgentName(), row.getPeriod(),
|
||||
toMinor(row.getTotalTargetAmount()), toMinor(row.getPendingTargetAmount()),
|
||||
toMinor(row.getOverdueTargetAmount()),
|
||||
toMinor(row.getTotalRepaymentAmount()), toMinor(row.getPendingRepaymentAmount()),
|
||||
toMinor(row.getOverdueRepaymentAmount()),
|
||||
toMinor(row.getTotalReceivableAmount()), toMinor(row.getPendingReceivableAmount()),
|
||||
toMinor(row.getOverdueReceivableAmount()),
|
||||
row.getTotalOrderCount(), row.getOverdueOrderCount(),
|
||||
row.getTotalCustomerCount(), row.getOverdueCustomerCount(),
|
||||
toMinor(row.getInvoiceAmount()), publicStatus(row.getStatus()),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
/** legacy Double(元)→ 分;null 原样返回 null。 */
|
||||
static Long toMinor(Double amount) {
|
||||
return amount == null ? null
|
||||
: BigDecimal.valueOf(amount).movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue();
|
||||
}
|
||||
|
||||
/** Legacy database status code to the stable public enum. */
|
||||
static String publicStatus(Integer status) {
|
||||
if (status == null) {
|
||||
throw invalidStatus(null);
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "INCOMPLETE";
|
||||
case 1 -> "COMPLETE";
|
||||
default -> throw invalidStatus(status);
|
||||
};
|
||||
}
|
||||
|
||||
private static ToolCallException invalidStatus(Integer status) {
|
||||
return new ToolCallException(McpErrorCode.DATA_INTEGRITY_ERROR,
|
||||
"回款目标完成状态无法映射到公开契约: " + status);
|
||||
}
|
||||
|
||||
private static String stableId(Long id) {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
}
|
||||
@@ -75,4 +75,66 @@ final class SyncChangeSchemas {
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
|
||||
/** 回款目标(销售 × 月):目标/还款/应收三组金额均为分。 */
|
||||
record RepaymentTargetChange(
|
||||
@JsonProperty("repayment_id") String repaymentId,
|
||||
@JsonProperty("owner_user_id") String ownerUserId,
|
||||
@JsonProperty("owner_name") String ownerName,
|
||||
@JsonProperty("agent_id") String agentId,
|
||||
@JsonProperty("agent_name") String agentName,
|
||||
String period,
|
||||
@JsonProperty("total_target_amount_minor") Long totalTargetAmountMinor,
|
||||
@JsonProperty("pending_target_amount_minor") Long pendingTargetAmountMinor,
|
||||
@JsonProperty("overdue_target_amount_minor") Long overdueTargetAmountMinor,
|
||||
@JsonProperty("total_repayment_amount_minor") Long totalRepaymentAmountMinor,
|
||||
@JsonProperty("pending_repayment_amount_minor") Long pendingRepaymentAmountMinor,
|
||||
@JsonProperty("overdue_repayment_amount_minor") Long overdueRepaymentAmountMinor,
|
||||
@JsonProperty("total_receivable_amount_minor") Long totalReceivableAmountMinor,
|
||||
@JsonProperty("pending_receivable_amount_minor") Long pendingReceivableAmountMinor,
|
||||
@JsonProperty("overdue_receivable_amount_minor") Long overdueReceivableAmountMinor,
|
||||
@JsonProperty("total_order_count") Integer totalOrderCount,
|
||||
@JsonProperty("overdue_order_count") Integer overdueOrderCount,
|
||||
@JsonProperty("total_customer_count") Integer totalCustomerCount,
|
||||
@JsonProperty("overdue_customer_count") Integer overdueCustomerCount,
|
||||
@JsonProperty("invoice_amount_minor") Long invoiceAmountMinor,
|
||||
String status,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
|
||||
/** 回款明细(客户 × 订单 × 周期):超期状态与超期日期为 SaaS 既有口径,只读透传。 */
|
||||
record RepaymentDetailChange(
|
||||
@JsonProperty("repayment_detail_id") String repaymentDetailId,
|
||||
@JsonProperty("owner_user_id") String ownerUserId,
|
||||
@JsonProperty("owner_name") String ownerName,
|
||||
@JsonProperty("saler_agent_id") String salerAgentId,
|
||||
@JsonProperty("saler_agent_name") String salerAgentName,
|
||||
@JsonProperty("property_id") String propertyId,
|
||||
@JsonProperty("property_name") String propertyName,
|
||||
@JsonProperty("district_id") String districtId,
|
||||
@JsonProperty("district_name") String districtName,
|
||||
@JsonProperty("customer_name") String customerName,
|
||||
String period,
|
||||
@JsonProperty("bill_period") String billPeriod,
|
||||
@JsonProperty("bill_type") String billType,
|
||||
@JsonProperty("order_no") String orderNo,
|
||||
@JsonProperty("bill_no") String billNo,
|
||||
@JsonProperty("overdue_status") String overdueStatus,
|
||||
@JsonProperty("overdue_at") String overdueAt,
|
||||
@JsonProperty("pending_receivable_amount_minor") Long pendingReceivableAmountMinor,
|
||||
@JsonProperty("overdue_receivable_amount_minor") Long overdueReceivableAmountMinor,
|
||||
@JsonProperty("total_receivable_amount_minor") Long totalReceivableAmountMinor,
|
||||
@JsonProperty("pending_repayment_amount_minor") Long pendingRepaymentAmountMinor,
|
||||
@JsonProperty("overdue_repayment_amount_minor") Long overdueRepaymentAmountMinor,
|
||||
@JsonProperty("total_repayment_amount_minor") Long totalRepaymentAmountMinor,
|
||||
@JsonProperty("invoice_amount_minor") Long invoiceAmountMinor,
|
||||
@JsonProperty("order_invoice_state") Integer orderInvoiceState,
|
||||
@JsonProperty("order_at") String orderAt,
|
||||
@JsonProperty("paid_at") String paidAt,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import me.iiv.agent.mcp.XqTool;
|
||||
import me.iiv.agent.mcp.sync.ChangesEngine;
|
||||
import me.iiv.agent.mcp.sync.ContractsSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.PropertiesSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.RepaymentDetailsSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.RepaymentTargetsSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.UsersSyncEntity;
|
||||
import me.iiv.agent.security.AuthContext;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
@@ -114,6 +116,36 @@ public class ChangesTools implements ToolObject {
|
||||
return result;
|
||||
}
|
||||
|
||||
@XqTool(category = "sync", kind = "read")
|
||||
@Tool(name = "repayment-targets.changes.list", description = "增量同步销售月度回款目标(m1.saler_repayment)。" + PROTOCOL_DESC
|
||||
+ "items 字段:repayment_id/owner_user_id(销售稳定 user.id)/owner_name/agent_id/agent_name/"
|
||||
+ "period(yyyy-mm)/目标、还款、应收三组金额(*_minor,分,各含 total/pending/overdue)/"
|
||||
+ "total_order_count/overdue_order_count/total_customer_count/overdue_customer_count/"
|
||||
+ "invoice_amount_minor/status(INCOMPLETE/COMPLETE)/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(销售非主管仅本人,其余按代理范围)。")
|
||||
public Map<String, Object> repaymentTargetsChangesList(
|
||||
@ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken,
|
||||
@ToolParam(required = false, description = CURSOR_DESC) String cursor,
|
||||
@ToolParam(required = false, description = LIMIT_DESC) Integer limit) {
|
||||
return changesEngine.execute(RepaymentTargetsSyncEntity.NAME, syncToken, cursor, limit);
|
||||
}
|
||||
|
||||
@XqTool(category = "sync", kind = "read")
|
||||
@Tool(name = "repayment-details.changes.list", description = "增量同步客户回款明细(m1.saler_repayment_customer,客户×订单×周期)。" + PROTOCOL_DESC
|
||||
+ "items 字段:repayment_detail_id/owner_user_id(销售稳定 user.id)/owner_name/"
|
||||
+ "saler_agent_id/saler_agent_name/property_id/property_name/district_id/district_name/customer_name/"
|
||||
+ "period/bill_period/bill_type(账期类型)/order_no/bill_no/"
|
||||
+ "overdue_status(NOT_OVERDUE/OVERDUE,SaaS 既有口径只读透传)/overdue_at/"
|
||||
+ "应收与还款金额(*_minor,分,各含 pending/overdue/total)/invoice_amount_minor/"
|
||||
+ "order_invoice_state(整型透传)/order_at/paid_at/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(与合同同一 ACL 语义)。")
|
||||
public Map<String, Object> repaymentDetailsChangesList(
|
||||
@ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken,
|
||||
@ToolParam(required = false, description = CURSOR_DESC) String cursor,
|
||||
@ToolParam(required = false, description = LIMIT_DESC) Integer limit) {
|
||||
return changesEngine.execute(RepaymentDetailsSyncEntity.NAME, syncToken, cursor, limit);
|
||||
}
|
||||
|
||||
private static List<Long> parseContractIds(List<String> contractIds) {
|
||||
if (contractIds == null || contractIds.isEmpty()) {
|
||||
throw new ToolCallException(McpErrorCode.INVALID_ARGUMENT, "contractIds必须包含1至200个合同ID");
|
||||
|
||||
@@ -11,6 +11,8 @@ import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.ToiletAirquality;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m1.SalerRepayment;
|
||||
import me.iiv.agent.entity.m1.SalerRepaymentCustomer;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.DeliveryOrder;
|
||||
import me.iiv.agent.entity.m2.InstallOrder;
|
||||
@@ -923,4 +925,41 @@ public class PermissionService {
|
||||
wrapper.in(ContractNew::getContractAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回款目标(m1 saler_repayment)行权限:总部/服务号全量;目标表无 property 维度,
|
||||
* 销售非主管退化为本人行(user_id=自己);其余按代理范围落到 agent_id。
|
||||
*/
|
||||
public void applyRepaymentTargetRowFilter(LambdaQueryWrapper<SalerRepayment> wrapper, User user) {
|
||||
RowScope scope = resolveRowScope(user);
|
||||
if (scope.headquarters) {
|
||||
return;
|
||||
}
|
||||
if (scope.salesPropertyIds != null) {
|
||||
wrapper.eq(SalerRepayment::getUserId, user.getId());
|
||||
return;
|
||||
}
|
||||
wrapper.in(SalerRepayment::getAgentId, scope.ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回款明细(m1 saler_repayment_customer)行权限:与合同同一 ACL 语义——
|
||||
* 销售非主管按 property_id 集合;其余按销售公司(saler_agent_id)+ 代管物业组合。
|
||||
*/
|
||||
public void applyRepaymentCustomerRowFilter(LambdaQueryWrapper<SalerRepaymentCustomer> wrapper, User user) {
|
||||
RowScope scope = resolveRowScope(user);
|
||||
if (scope.headquarters) {
|
||||
return;
|
||||
}
|
||||
if (scope.salesPropertyIds != null) {
|
||||
wrapper.in(SalerRepaymentCustomer::getPropertyId, scope.salesPropertyIds);
|
||||
return;
|
||||
}
|
||||
if (!scope.managedPropertyIds.isEmpty()) {
|
||||
wrapper.and(w -> w.in(SalerRepaymentCustomer::getSalerAgentId, scope.ids)
|
||||
.or().in(SalerRepaymentCustomer::getPropertyId, scope.managedPropertyIds));
|
||||
} else {
|
||||
wrapper.in(SalerRepaymentCustomer::getSalerAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.SalerRepayment;
|
||||
import me.iiv.agent.entity.m1.SalerRepaymentCustomer;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
import me.iiv.agent.mapper.m1.SalerRepaymentCustomerMapper;
|
||||
import me.iiv.agent.mapper.m1.SalerRepaymentMapper;
|
||||
import me.iiv.agent.mapper.m2.ContractNewMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
@@ -50,6 +54,10 @@ class SyncEntitiesTest {
|
||||
@Mock
|
||||
ContractNewMapper contractNewMapper;
|
||||
@Mock
|
||||
SalerRepaymentMapper salerRepaymentMapper;
|
||||
@Mock
|
||||
SalerRepaymentCustomerMapper salerRepaymentCustomerMapper;
|
||||
@Mock
|
||||
PermissionService permissionService;
|
||||
|
||||
@BeforeEach
|
||||
@@ -57,6 +65,8 @@ class SyncEntitiesTest {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), User.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Properyt.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), ContractNew.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), SalerRepayment.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), SalerRepaymentCustomer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -291,4 +301,122 @@ class SyncEntitiesTest {
|
||||
assertEquals("2026-09-15T09:00:00+08:00", items.get(0).get("deleted_at"));
|
||||
verify(permissionService, never()).applyContractRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repaymentTargetsMapsAmountsToMinorAndStatus() {
|
||||
RepaymentTargetsSyncEntity entity = new RepaymentTargetsSyncEntity(salerRepaymentMapper, permissionService);
|
||||
SalerRepayment target = new SalerRepayment();
|
||||
target.setId(5L);
|
||||
target.setUserId(1001L);
|
||||
target.setUserName("张销售");
|
||||
target.setAgentId(21L);
|
||||
target.setAgentName("趣致北京");
|
||||
target.setPeriod("2026-10");
|
||||
target.setTotalTargetAmount(200000.00);
|
||||
target.setOverdueTargetAmount(50000.005);
|
||||
target.setTotalRepaymentAmount(120000.10);
|
||||
target.setOverdueReceivableAmount(30000.00);
|
||||
target.setTotalOrderCount(8);
|
||||
target.setOverdueOrderCount(2);
|
||||
target.setTotalCustomerCount(5);
|
||||
target.setOverdueCustomerCount(1);
|
||||
target.setStatus(0);
|
||||
target.setSoftDelete(0);
|
||||
target.setModifyTime(Timestamp.valueOf("2026-10-10 09:00:00"));
|
||||
when(salerRepaymentMapper.selectList(any())).thenReturn(List.of(target));
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
|
||||
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyRepaymentTargetRowFilter(any(), any());
|
||||
Map<String, Object> item = rows.get(0).item();
|
||||
assertEquals("5", item.get("repayment_id"));
|
||||
assertEquals("1001", item.get("owner_user_id"));
|
||||
assertEquals("张销售", item.get("owner_name"));
|
||||
assertEquals("2026-10", item.get("period"));
|
||||
assertEquals(20000000L, item.get("total_target_amount_minor"), "金额按分输出");
|
||||
assertEquals(5000001L, item.get("overdue_target_amount_minor"), "legacy Double 转分四舍五入");
|
||||
assertEquals(12000010L, item.get("total_repayment_amount_minor"));
|
||||
assertEquals(3000000L, item.get("overdue_receivable_amount_minor"));
|
||||
assertEquals(8, item.get("total_order_count"));
|
||||
assertEquals(2, item.get("overdue_order_count"));
|
||||
assertEquals("INCOMPLETE", item.get("status"));
|
||||
assertNull(item.get("deleted_at"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repaymentTargetsRejectsUnknownStatus() {
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> RepaymentTargetsSyncEntity.publicStatus(7));
|
||||
|
||||
assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repaymentDetailsMapsOverdueFieldsAndTombstone() {
|
||||
RepaymentDetailsSyncEntity entity = new RepaymentDetailsSyncEntity(salerRepaymentCustomerMapper, permissionService);
|
||||
SalerRepaymentCustomer detail = new SalerRepaymentCustomer();
|
||||
detail.setId(31L);
|
||||
detail.setSalerUserId(1001L);
|
||||
detail.setSalerUserName("张销售");
|
||||
detail.setSalerAgentId(21L);
|
||||
detail.setPropertyId(9L);
|
||||
detail.setPropertyName("望京SOHO");
|
||||
detail.setDistrictId(3L);
|
||||
detail.setCustomerName("金蝶客户名称");
|
||||
detail.setPeriod("2026-10");
|
||||
detail.setBillPeriod("2026-09");
|
||||
detail.setBillType("月结30天");
|
||||
detail.setOrderNo("SO20260901");
|
||||
detail.setBillNo("B20260901-1");
|
||||
detail.setOverdueStatus(1);
|
||||
detail.setOverdueTime(Timestamp.valueOf("2026-10-01 00:00:00"));
|
||||
detail.setOverdueReceivableAmount(8000.00);
|
||||
detail.setTotalReceivableAmount(20000.00);
|
||||
detail.setTotalRepaymentAmount(12000.00);
|
||||
detail.setOrderInvoiceState(2);
|
||||
detail.setOrderTime(Timestamp.valueOf("2026-09-01 10:00:00"));
|
||||
detail.setSoftDelete(1);
|
||||
detail.setModifyTime(Timestamp.valueOf("2026-10-11 08:30:00"));
|
||||
when(salerRepaymentCustomerMapper.selectList(any())).thenReturn(List.of(detail));
|
||||
|
||||
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, null);
|
||||
|
||||
verify(permissionService, never()).applyRepaymentCustomerRowFilter(any(), any());
|
||||
Map<String, Object> item = rows.get(0).item();
|
||||
assertEquals("31", item.get("repayment_detail_id"));
|
||||
assertEquals("1001", item.get("owner_user_id"), "销售=saler_user_id 稳定 user.id");
|
||||
assertEquals("9", item.get("property_id"));
|
||||
assertEquals("望京SOHO", item.get("property_name"));
|
||||
assertEquals("金蝶客户名称", item.get("customer_name"));
|
||||
assertEquals("月结30天", item.get("bill_type"), "账期类型只读透传");
|
||||
assertEquals("OVERDUE", item.get("overdue_status"));
|
||||
assertEquals("2026-10-01T00:00:00+08:00", item.get("overdue_at"));
|
||||
assertEquals(800000L, item.get("overdue_receivable_amount_minor"));
|
||||
assertEquals(2000000L, item.get("total_receivable_amount_minor"));
|
||||
assertEquals(2, item.get("order_invoice_state"), "开票状态整型透传,不造枚举");
|
||||
assertEquals("2026-09-01T10:00:00+08:00", item.get("order_at"));
|
||||
assertEquals("2026-10-11T08:30:00+08:00", item.get("deleted_at"), "软删行返回 tombstone");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repaymentDetailsAppliesRowFilterForUserToken() {
|
||||
RepaymentDetailsSyncEntity entity = new RepaymentDetailsSyncEntity(salerRepaymentCustomerMapper, permissionService);
|
||||
when(salerRepaymentCustomerMapper.selectList(any())).thenReturn(List.of());
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
|
||||
entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyRepaymentCustomerRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repaymentDetailsRejectsUnknownOverdueStatus() {
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> RepaymentDetailsSyncEntity.publicOverdueStatus(9));
|
||||
|
||||
assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 三个 changes 工具的注册与路由(REQ-20260911-0013)。
|
||||
* 五个 changes 工具的注册与路由(REQ-20260911-0013,回款域扩展于 REQ-20260902-0002 P2)。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChangesToolsTest {
|
||||
@@ -41,8 +41,9 @@ class ChangesToolsTest {
|
||||
callback -> callback.getToolDefinition().name(),
|
||||
callback -> callback.getToolDefinition().inputSchema()));
|
||||
|
||||
assertEquals(4, schemas.size());
|
||||
for (String name : new String[]{"users.changes.list", "properties.changes.list", "contracts.changes.list"}) {
|
||||
assertEquals(6, schemas.size());
|
||||
for (String name : new String[]{"users.changes.list", "properties.changes.list", "contracts.changes.list",
|
||||
"repayment-targets.changes.list", "repayment-details.changes.list"}) {
|
||||
assertTrue(schemas.containsKey(name), "缺少工具 " + name);
|
||||
String schema = schemas.get(name);
|
||||
assertTrue(schema.contains("syncToken"));
|
||||
@@ -68,6 +69,12 @@ class ChangesToolsTest {
|
||||
|
||||
tools.contractsChangesList("t2", null, 200);
|
||||
verify(changesEngine).execute("contracts", "t2", null, 200);
|
||||
|
||||
tools.repaymentTargetsChangesList("t3", "c3", null);
|
||||
verify(changesEngine).execute("repayment-targets", "t3", "c3", null);
|
||||
|
||||
tools.repaymentDetailsChangesList(null, null, 100);
|
||||
verify(changesEngine).execute("repayment-details", null, null, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user