Compare commits

..
Author SHA1 Message Date
xiaoqu-ai 10982f24a3 fix(mcp): 修正物业所属销售富化永远为空(Map key 被驼峰化)
根因:SQL 别名用 property_id/user_id/user_name,而 MyBatis 的
mapUnderscoreToCamelCase=true 会把结果 Map 的 key 转成 propertyId/userId/userName,
代码按 "property_id" 取值永远 null → 富化静默跳过,sale_user_id 恒为空。

修复:
- 别名改为无下划线的 pid/uid/uname,绕开驼峰转换
- 单测补两条并锁住:取到 userId → sale_user_id;缺失时回落 create_id
  (此前构造器新增 AgentMapper 参数,测试未同步,已一并修正)

验证:SyncEntitiesTest 26 例全过。
2026-10-20 18:40:44 +08:00
xiaoqu-ai 99adb3dfc1 feat(mcp): properties 流导出物业所属销售 sale_user_id / sale_user_name
物业表的所属销售列是 userId / userName(实体未映射),此前一直取不到。

- ProperytMapper.selectPropertyOwners:显式 SQL 按 id 批量取 userId/userName
- PropertiesSyncEntity 批量富化,**独立查询 + try/catch 容错**:列名或数据异常
  只丢归属,不影响 properties 变更流
- 流字段:sale_user_id / sale_user_name(缺失时回落到 create_id)

依据:PermissionService.resolveRowScope 用 properyt.create_id=user.id 判定销售可见物业,
说明物业归属销售;本次直接取物业表的 userId 列,命中即为该物业的所属销售。
2026-10-20 18:29:06 +08:00
xiaoqu-ai 3da52f55ec feat(mcp): properties 流导出 create_id —— 物业所属销售即创建人(对齐 PermissionService 判定) 2026-10-20 18:23:40 +08:00
xiaoqu-ai 0ea48ee1fb feat(mcp): properties 流带出所属销售 sale_id / sale_name
所属销售不在物业表上,而在代理商表:property.agent_id → Agent.saleId(注释即"销售id")/
Agent.saleName。supervisorId 实测全空,不能作归属。

- PropertiesSyncEntity 注入 AgentMapper,按页批量富化(一次 selectBatchIds,无 N+1)
- PropertyChange 增加 sale_id / sale_name

下游:CRM 用 sale_id(SaaS 用户 id)经 user 映射解析为客户负责人。
2026-10-20 18:18:38 +08:00
xiaoqu-ai 49a8a2ddff feat(mcp): properties 流导出归属字段 supervisor_id / agent_id
问题:m0.properyt 有 supervisorId(所属销售)与 agentId(所属代理商),
但 properties 变更流只导出 10 个字段,下游拿不到归属,物业建出的客户无负责人。

改动:
- SyncChangeSchemas.PropertyChange 增加 supervisor_id、agent_id
- PropertiesSyncEntity.toItem 传出 row.getSupervisorId()/getAgentId()

下游:网关透传 → CRM 按 user 映射解析为负责人(解析不到保持公海)。
2026-10-20 18:05:15 +08:00
4 changed files with 145 additions and 23 deletions
@@ -1,16 +1,34 @@
package me.iiv.agent.mapper.m0;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m0.Properyt;
/**
* <p>
* 物业表 Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-08
*/
public interface ProperytMapper extends BaseMapper<Properyt> {
}
package me.iiv.agent.mapper.m0;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m0.Properyt;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* <p>
* 物业表 Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-08
*/
public interface ProperytMapper extends BaseMapper<Properyt> {
/**
* 按 id 批量取物业的"所属销售"。
*
* <p>来源是物业表的 userId / userName 两列(实体未映射,故用显式 SQL)。
* 独立查询 + 调用方容错:列名或数据异常只丢归属,不影响 properties 变更流。</p>
*/
// 别名刻意用无下划线的名字:MyBatis 的 mapUnderscoreToCamelCase 会把
// property_id → propertyId,导致按 "property_id" 取值永远为 null。
@Select("<script>SELECT id AS pid, userId AS uid, userName AS uname"
+ " FROM properyt WHERE id IN"
+ " <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>"
+ "</script>")
List<Map<String, Object>> selectPropertyOwners(@Param("ids") List<Long> ids);
}
@@ -27,10 +27,13 @@ public class PropertiesSyncEntity extends AbstractSyncEntity implements SyncEnti
final ProperytMapper properytMapper;
final PermissionService permissionService;
final me.iiv.agent.mapper.m0.AgentMapper agentMapper;
public PropertiesSyncEntity(ProperytMapper properytMapper, PermissionService permissionService) {
public PropertiesSyncEntity(ProperytMapper properytMapper, PermissionService permissionService,
me.iiv.agent.mapper.m0.AgentMapper agentMapper) {
this.properytMapper = properytMapper;
this.permissionService = permissionService;
this.agentMapper = agentMapper;
}
@Override
@@ -47,20 +50,74 @@ public class PropertiesSyncEntity extends AbstractSyncEntity implements SyncEnti
}
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
applyOrderAndLimit(wrapper, limit);
List<Properyt> list = properytMapper.selectList(wrapper);
// 物业的"所属销售"在代理商表上(Agent.saleId/saleName),这里按页批量富化,避免 N+1。
java.util.Set<Long> agentIds = new java.util.HashSet<>();
for (Properyt row : list) {
if (row.getAgentId() != null) {
agentIds.add(row.getAgentId());
}
}
Map<Long, me.iiv.agent.entity.m0.Agent> agents = new java.util.HashMap<>();
if (!agentIds.isEmpty()) {
for (me.iiv.agent.entity.m0.Agent agent : agentMapper.selectBatchIds(agentIds)) {
agents.put(agent.getId(), agent);
}
}
// 物业"所属销售":优先物业表 userId/userName(实体未映射,独立查询 + 容错)。
Map<Long, String> ownerIds = new java.util.HashMap<>();
Map<Long, String> ownerNames = new java.util.HashMap<>();
try {
List<Long> ids = new java.util.ArrayList<>();
for (Properyt row : list) {
if (row.getId() != null) {
ids.add(row.getId());
}
}
if (!ids.isEmpty()) {
for (Map<String, Object> owner : properytMapper.selectPropertyOwners(ids)) {
Object pid = owner.get("pid");
if (pid == null) {
continue;
}
Long propertyId = Long.valueOf(String.valueOf(pid));
Object uid = owner.get("uid");
Object uname = owner.get("uname");
if (uid != null) {
ownerIds.put(propertyId, String.valueOf(uid));
}
if (uname != null) {
ownerNames.put(propertyId, String.valueOf(uname));
}
}
}
} catch (Exception enrichmentError) {
// 列名/权限异常时只丢归属,不能让整条 properties 流失败。
org.slf4j.LoggerFactory.getLogger(PropertiesSyncEntity.class)
.warn("物业所属销售富化失败,跳过归属: {}", enrichmentError.getMessage());
}
List<SyncRow> rows = new ArrayList<>();
for (Properyt row : properytMapper.selectList(wrapper)) {
for (Properyt row : list) {
long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime());
rows.add(new SyncRow(toItem(row, mt), mt, row.getId()));
rows.add(new SyncRow(toItem(row, mt, agents.get(row.getAgentId()),
ownerIds.get(row.getId()), ownerNames.get(row.getId())), mt, row.getId()));
}
return rows;
}
private Map<String, Object> toItem(Properyt row, long mt) {
private Map<String, Object> toItem(Properyt row, long mt, me.iiv.agent.entity.m0.Agent agent,
String ownerUserId, String ownerUserName) {
boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1;
Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime();
return SyncChangeSchemas.toMap(new SyncChangeSchemas.PropertyChange(
stableId(row.getId()), row.getCustomerId(), row.getName(), row.getJinideName(),
row.getIdentificationNumber(), row.getSerialnumber(), deleted ? "INACTIVE" : "ACTIVE",
row.getIdentificationNumber(), row.getSerialnumber(),
stableId(row.getSupervisorId()), stableId(row.getAgentId()),
ownerUserId == null ? stableId(row.getCreateId()) : ownerUserId,
ownerUserName,
agent == null ? null : stableId(agent.getSaleId()),
agent == null ? null : agent.getSaleName(),
deleted ? "INACTIVE" : "ACTIVE",
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
}
@@ -47,6 +47,12 @@ final class SyncChangeSchemas {
@JsonProperty("customer_name") String customerName,
@JsonProperty("unified_social_credit_code") String unifiedSocialCreditCode,
@JsonProperty("external_customer_no") String externalCustomerNo,
@JsonProperty("supervisor_id") String supervisorId,
@JsonProperty("agent_id") String agentId,
@JsonProperty("sale_user_id") String saleUserId,
@JsonProperty("sale_user_name") String saleUserName,
@JsonProperty("sale_id") String saleId,
@JsonProperty("sale_name") String saleName,
String status,
@JsonProperty("updated_at") String updatedAt,
long version,
@@ -18,6 +18,7 @@ import me.iiv.agent.entity.m2.WithdrawOrder;
import me.iiv.agent.mapper.m0.BatteryFaultLogMapper;
import me.iiv.agent.mapper.m0.DeviceLifeCycleMapper;
import me.iiv.agent.mapper.m0.HardwareViewMapper;
import me.iiv.agent.mapper.m0.AgentMapper;
import me.iiv.agent.mapper.m0.ProperytMapper;
import me.iiv.agent.mapper.m0.UserMapper;
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
@@ -67,6 +68,8 @@ class SyncEntitiesTest {
@Mock
ProperytMapper properytMapper;
@Mock
AgentMapper agentMapper;
@Mock
ContractNewMapper contractNewMapper;
@Mock
SalerRepaymentMapper salerRepaymentMapper;
@@ -166,7 +169,7 @@ class SyncEntitiesTest {
@Test
void propertiesAppliesRowFilterForUserAndMapsFields() {
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService);
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService, agentMapper);
Properyt property = new Properyt();
property.setId(9L);
property.setName("望京SOHO");
@@ -192,9 +195,47 @@ class SyncEntitiesTest {
assertEquals("ACTIVE", rows.get(0).item().get("status"));
}
@Test
void propertiesExportsSaleOwnerFromUserIdColumns() {
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService, agentMapper);
Properyt property = new Properyt();
property.setId(9L);
property.setName("望京SOHO");
property.setAgentId(33L);
property.setCreateId(11L);
property.setSoftDelete(0);
property.setModifyTime(Timestamp.valueOf("2026-09-01 10:00:00"));
when(properytMapper.selectList(any())).thenReturn(List.of(property));
// 别名刻意无下划线:MyBatis 会把 property_id 转成 propertyId
when(properytMapper.selectPropertyOwners(any()))
.thenReturn(List.of(Map.of("pid", 9L, "uid", 77L, "uname", "张三")));
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, null);
assertEquals("77", rows.get(0).item().get("sale_user_id"), "物业所属销售取 userId 列");
assertEquals("张三", rows.get(0).item().get("sale_user_name"));
}
@Test
void propertiesFallsBackToCreateIdWhenOwnerMissing() {
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService, agentMapper);
Properyt property = new Properyt();
property.setId(9L);
property.setName("望京SOHO");
property.setCreateId(11L);
property.setSoftDelete(0);
property.setModifyTime(Timestamp.valueOf("2026-09-01 10:00:00"));
when(properytMapper.selectList(any())).thenReturn(List.of(property));
when(properytMapper.selectPropertyOwners(any())).thenReturn(List.of());
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, null);
assertEquals("11", rows.get(0).item().get("sale_user_id"), "缺 userId 时回落到创建人");
}
@Test
void propertiesSkipsRowFilterForServiceToken() {
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService);
PropertiesSyncEntity entity = new PropertiesSyncEntity(properytMapper, permissionService, agentMapper);
when(properytMapper.selectList(any())).thenReturn(List.of());
entity.fetchPage(null, 2000L, null, null, 3, null);