diff --git a/xiaoqu-agent-service/README.md b/xiaoqu-agent-service/README.md index e6d4b68..2db5d26 100644 --- a/xiaoqu-agent-service/README.md +++ b/xiaoqu-agent-service/README.md @@ -2,7 +2,7 @@ 面向 MCP 客户端(Kimi Code / Claude 等)的小趣业务数据服务:独立 Spring Boot 应用,只读查询现有三库(m0 权限库 / m1 商城库 / m2 业务库),以 MCP(Streamable HTTP)形式对外暴露工具。 -设计方案见 `../xiaoqu-franchisee/doc/agent-mcp-service-design.md`。当前进度:**P2 权限层已完成**(ak 鉴权、scope 校验、行级数据权限引擎、审计落库,均经 curl 实测)。管理后台为 P4,未实现。 +设计方案见 `../xiaoqu-franchisee/doc/agent-mcp-service-design.md`。当前进度:**P3 首批工具完成**(P1 骨架 + P2 权限层 + agent.list.query / property.list.query 完整版,均经 curl 实测)。管理后台为 P4,未实现。 ## 技术栈与已验证版本组合 @@ -72,7 +72,10 @@ xiaoqu-agent-service/ |---|---|---|---| | `sys.ping` | system | read | 服务自检:服务名、版本、当前时间、Redis PING | | `agent.count` | system | read | 查 m0 库 `agent` 表 `soft_delete=0` 总数 | -| `agent.list.query` | customer | read | 代理商分页查询(行级权限),入参 id/name/level/channelNo/page/rows | +| `agent.list.query` | customer | read | 代理商分页查询。入参 id/name/level/channelNo/province/city/serialnumber/page/rows;出参 id/name/level/channelNo/serialnumber/supervisorName/province/city/countyseat/contact/phone/agencyarea/memo/createTime。行权限:isagent=1 只查直属下级,id 精确查可看自身。supervisorName 批量 IN 富化 | +| `property.list.query` | customer | read | 物业(客户)分页查询(m0 properyt 表)。入参 name/agentId/serialnumber/province/city/contact/phone/page/rows;出参 id/name/serialnumber/agentName/createName/province/city/countyseat/address/contact/phone/createTime。行权限走 PermissionService 完整版 getByIsAgent 语义。agentName/createName 批量 IN 富化 | + +分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。 工具名使用点号分隔(MCP 规范允许 `[a-zA-Z0-9._-]`,实测 Spring AI 1.1.8 与握手流程均正常)。工具的 category/kind 由代码 `@XqTool` 注解声明,启动时同步进 `tool` 表(只插不删,管理员停用状态不会被覆盖)。新工具默认不属于任何作用域 = 任何 token 调不到,需管理员配 scope。 @@ -88,6 +91,8 @@ mysql -h192.168.1.181 -uroot -p xiaoqu_agent_svc_d < sql/seed-dev.sql java -jar target/xiaoqu-agent-service.jar # 或 ./mvnw spring-boot:run +# 后台常驻(本机兼任 dev 部署机时): +nohup java -jar target/xiaoqu-agent-service.jar > target/app.log 2>&1 & ``` - 默认 `spring.profiles.active=dev`,端口 **8088**,MCP 端点 `http://localhost:8088/mcp`。 @@ -123,6 +128,43 @@ curl -X POST http://localhost:8088/mcp \ 响应为 SSE 帧(`event: message` / `data: {...}`)。2026-09-03 实测:无 ak/无效 ak → 401;总部 ak 查 agent.list.query total=62(全量);代理商 ak total=26(与 `SELECT COUNT(*) FROM agent WHERE soft_delete=0 AND supervisor_id=1` 一致);无 scope 的 ak → `isError:true` + "该 token 的作用域不包含工具 [agent.list.query],请联系管理员在对应作用域开通";audit_log 记录 success/denied 与 client_type。 +property.list.query 权限隔离实测(2026-09-03,均与直连 SQL 核对一致): + +| 条件 | 总部 ak | 代理商 ak | +|---|---|---| +| 无过滤 | total=141 | total=73 | +| province=北京市 | total=38 | total=31 | +| name 模糊 '%测试%' | — | total=7(与 SQL 一致) | + +agent.list.query 抽测:channel_no='D' + province='北京市' → 总部 11 / 代理商 9(均与 SQL 一致);id=79 精确查 → 1;rows=500 → 截断为 100 并返回 notice。 + +## 客户端接入(dev 阶段裸跑 HTTP) + +dev 服务地址示例:`http://192.168.4.236:8088/mcp`(本机内网 IP;本机自测可用 localhost)。生产必须 HTTPS(Nginx 终结 TLS)。 + +**Kimi Code** — 项目级 `.kimi-code/mcp.json`: + +```json +{ + "mcpServers": { + "xiaoqu": { + "url": "http://192.168.4.236:8088/mcp", + "bearerTokenEnvVar": "XQ_API_KEY" + } + } +} +``` + +shell 里 `export XQ_API_KEY=<测试ak>` 后启动 Kimi Code 即可自发现工具。 + +**Claude Code**: + +```bash +export XQ_API_KEY=<测试ak> +claude mcp add --transport http xiaoqu http://192.168.4.236:8088/mcp \ + --header "Authorization: Bearer $XQ_API_KEY" +``` + ## codegen(run-once) 表白名单:m0 `user, agent, authority, properyt, agent_manage_district_auth`;m2 `warehouse_agent_auth`;svc `agent_api_key, agent_scope, tool, scope_tool, token_scope, audit_log, admin_user`(注:物业表真实表名为老系统遗留拼写 `properyt`)。生成物落在 `src/main/java` 对应分包并提交进仓库。 diff --git a/xiaoqu-agent-service/sql/seed-dev.sql b/xiaoqu-agent-service/sql/seed-dev.sql index 5976331..badc740 100644 --- a/xiaoqu-agent-service/sql/seed-dev.sql +++ b/xiaoqu-agent-service/sql/seed-dev.sql @@ -31,10 +31,10 @@ INSERT INTO agent_scope (name, code, memo, status, create_id) SELECT '运营', 'operation', 'dev 种子:运营作用域', 1, 0 WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'operation'); --- 作用域-工具:sys.ping / agent.count / agent.list.query +-- 作用域-工具:sys.ping / agent.count / agent.list.query / property.list.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') +WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query') AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id); -- token-作用域:HQ 与 AGENT 两把 token 授权"运营";NOSCOPE 不授权 diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/AgentTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/AgentTools.java index c25629b..4e080aa 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/AgentTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/AgentTools.java @@ -18,10 +18,11 @@ 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; /** - * 代理商域工具:agent.count(demo)+ agent.list.query(行级权限验证用基础版, - * 语义照搬老系统 AgentController.listByPage)。 + * 代理商域工具:agent.count(demo)+ agent.list.query(对应老系统 AgentController.listByPage)。 */ @Component public class AgentTools implements ToolObject { @@ -51,19 +52,28 @@ public class AgentTools implements ToolObject { * 查询代理商列表。行权限照搬老系统 AgentController.listByPage: * 权限组 isagent=1 时只查直属下级(supervisor_id = 用户 agentId);isagent=0 不过滤。 * 入参传 id 时精确查询(可查到自身,此时不加 supervisor 限制)。 + * supervisorName 批量 IN 查询富化,无 N+1。 */ @XqTool(category = "customer", kind = "read") - @Tool(name = "agent.list.query", description = "查询代理商列表(分页)。代理商身份的用户只能查到直属下级代理;" - + "总部身份看全部。传 id 时为精确查询。") + @Tool(name = "agent.list.query", description = "查询代理商列表(分页)。" + + "行级权限:代理商身份的用户(权限组 isagent=1)只能查到自己名下的直属下级代理;总部身份(isagent=0)可看全部。" + + "传 id 时为精确查询、可查到自身,其余过滤条件失效。" + + "level 含义:0=总部 1=一级 2=二级;channelNo 含义:Z=总部 F=分公司 D=代理。" + + "分页:page 从 1 开始,rows 默认 20、上限 100,超出自动截断为 100 并在返回的 notice 字段提示。" + + "返回 total(符合条件的总数)、records(本页记录)。") public Map agentListQuery( - @ToolParam(required = false, description = "代理商 id,精确查询(可查到自身)") Long id, + @ToolParam(required = false, description = "代理商 id,精确查询(可查到自身);传入后忽略其他过滤条件") Long id, @ToolParam(required = false, description = "代理商名称,模糊匹配") String name, - @ToolParam(required = false, description = "级别:0总部/1一级/2二级") Integer level, - @ToolParam(required = false, description = "渠道编号:Z/F/D") String channelNo, - @ToolParam(required = false, description = "页码,默认 1") Integer page, - @ToolParam(required = false, description = "每页条数,默认 20,上限 100") Integer rows) { + @ToolParam(required = false, description = "级别:0=总部 1=一级 2=二级") Integer level, + @ToolParam(required = false, description = "渠道编号:Z=总部 F=分公司 D=代理") String channelNo, + @ToolParam(required = false, description = "省份,精确匹配,如 北京市") String province, + @ToolParam(required = false, description = "城市,精确匹配,如 北京市") String city, + @ToolParam(required = false, description = "代理商编号(serialnumber),精确匹配") String serialnumber, + @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() @@ -80,6 +90,15 @@ public class AgentTools implements ToolObject { if (StringUtils.hasText(channelNo)) { wrapper.eq(Agent::getChannelNo, channelNo); } + if (StringUtils.hasText(province)) { + wrapper.eq(Agent::getProvince, province); + } + if (StringUtils.hasText(city)) { + wrapper.eq(Agent::getCity, city); + } + if (StringUtils.hasText(serialnumber)) { + wrapper.eq(Agent::getSerialnumber, serialnumber); + } if (permissionService.isAgentAuthority(user.getAuthorityId())) { wrapper.eq(Agent::getSupervisorId, user.getAgentId()); } @@ -87,12 +106,25 @@ public class AgentTools implements ToolObject { Long total = agentMapper.selectCount(wrapper); wrapper.select(Agent::getId, Agent::getName, Agent::getLevel, Agent::getChannelNo, - Agent::getSerialnumber, Agent::getProvince, Agent::getCity, - Agent::getContact, Agent::getPhone, Agent::getCreateTime) + Agent::getSerialnumber, Agent::getSupervisorId, Agent::getProvince, Agent::getCity, + Agent::getCountyseat, Agent::getContact, Agent::getPhone, Agent::getAgencyarea, + Agent::getMemo, Agent::getCreateTime) .orderByDesc(Agent::getId) .last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize); List agents = agentMapper.selectList(wrapper); + // supervisorName 批量富化(一次 IN 查询,消除 N+1) + List supervisorIds = agents.stream() + .map(Agent::getSupervisorId) + .filter(Objects::nonNull) + .filter(sid -> sid != 0) + .distinct() + .collect(Collectors.toList()); + Map supervisorNames = supervisorIds.isEmpty() + ? Map.of() + : agentMapper.selectBatchIds(supervisorIds).stream() + .collect(Collectors.toMap(Agent::getId, Agent::getName, (a, b) -> a)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); List> records = new ArrayList<>(); for (Agent agent : agents) { @@ -102,10 +134,16 @@ public class AgentTools implements ToolObject { record.put("level", agent.getLevel()); record.put("channelNo", agent.getChannelNo()); record.put("serialnumber", agent.getSerialnumber()); + record.put("supervisorName", + agent.getSupervisorId() == null || agent.getSupervisorId() == 0 + ? "-" : supervisorNames.getOrDefault(agent.getSupervisorId(), "-")); record.put("province", agent.getProvince()); record.put("city", agent.getCity()); + record.put("countyseat", agent.getCountyseat()); record.put("contact", agent.getContact()); record.put("phone", agent.getPhone()); + record.put("agencyarea", agent.getAgencyarea()); + record.put("memo", agent.getMemo()); record.put("createTime", agent.getCreateTime() == null ? null : sdf.format(agent.getCreateTime())); records.add(record); } @@ -114,6 +152,9 @@ public class AgentTools implements ToolObject { 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; } diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/PropertyTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/PropertyTools.java new file mode 100644 index 0000000..8f7b0de --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/PropertyTools.java @@ -0,0 +1,152 @@ +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.Properyt; +import me.iiv.agent.entity.m0.User; +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.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; + +/** + * 物业(客户)域工具:property.list.query(对应老系统 PropertyController.listByPage, + * 行权限走 PermissionService.applyPropertyRowFilter)。 + */ +@Component +public class PropertyTools implements ToolObject { + + private static final int DEFAULT_ROWS = 20; + private static final int MAX_ROWS = 100; + + final ProperytMapper properytMapper; + final AgentMapper agentMapper; + final UserMapper userMapper; + final PermissionService permissionService; + + public PropertyTools(ProperytMapper properytMapper, AgentMapper agentMapper, + UserMapper userMapper, PermissionService permissionService) { + this.properytMapper = properytMapper; + this.agentMapper = agentMapper; + this.userMapper = userMapper; + this.permissionService = permissionService; + } + + @XqTool(category = "customer", kind = "read") + @Tool(name = "property.list.query", description = "查询物业(客户)列表(分页),数据来源 m0 库 properyt 表。" + + "行级权限:代理商身份的用户(权限组 isagent=1)只能看到 自己+直属下级代理+被授权代管代理 名下的物业," + + "以及被授权代管的物业(agent_manage_district_auth);总部身份(isagent=0)可看全部;" + + "物业账号(enterprise_type=2)只能看自己物业;销售部门非负责人只能看自己创建的物业。" + + "分页:page 从 1 开始,rows 默认 20、上限 100,超出自动截断为 100 并在返回的 notice 字段提示。" + + "返回 total(符合条件的总数)、records(本页记录)。") + public Map propertyListQuery( + @ToolParam(required = false, description = "物业名称,模糊匹配") String name, + @ToolParam(required = false, description = "所属代理商 id,精确匹配") Long agentId, + @ToolParam(required = false, description = "物业编号(serialnumber),精确匹配") String serialnumber, + @ToolParam(required = false, description = "省份,精确匹配,如 北京市") String province, + @ToolParam(required = false, description = "城市,精确匹配,如 北京市") String city, + @ToolParam(required = false, description = "联系人,精确匹配") String contact, + @ToolParam(required = false, description = "联系电话,精确匹配") String phone, + @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(Properyt::getSoftDelete, 0); + if (StringUtils.hasText(name)) { + wrapper.like(Properyt::getName, name); + } + if (agentId != null) { + wrapper.eq(Properyt::getAgentId, agentId); + } + if (StringUtils.hasText(serialnumber)) { + wrapper.eq(Properyt::getSerialnumber, serialnumber); + } + if (StringUtils.hasText(province)) { + wrapper.eq(Properyt::getProvince, province); + } + if (StringUtils.hasText(city)) { + wrapper.eq(Properyt::getCity, city); + } + if (StringUtils.hasText(contact)) { + wrapper.eq(Properyt::getContact, contact); + } + if (StringUtils.hasText(phone)) { + wrapper.eq(Properyt::getPhone, phone); + } + // 行级权限(完整版 getByIsAgent 语义,统一走 PermissionService) + permissionService.applyPropertyRowFilter(wrapper, user); + + Long total = properytMapper.selectCount(wrapper); + wrapper.select(Properyt::getId, Properyt::getName, Properyt::getSerialnumber, + Properyt::getAgentId, Properyt::getCreateId, Properyt::getProvince, Properyt::getCity, + Properyt::getCountyseat, Properyt::getAddress, Properyt::getContact, Properyt::getPhone, + Properyt::getCreateTime) + .orderByDesc(Properyt::getId) + .last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize); + List properties = properytMapper.selectList(wrapper); + + // agentName / createName 批量富化(一次 IN 查询,消除 N+1) + List agentIds = properties.stream().map(Properyt::getAgentId) + .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 = properties.stream().map(Properyt::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"); + List> records = new ArrayList<>(); + for (Properyt properyt : properties) { + Map record = new LinkedHashMap<>(); + record.put("id", properyt.getId()); + record.put("name", properyt.getName()); + record.put("serialnumber", properyt.getSerialnumber()); + record.put("agentName", properyt.getAgentId() == null + ? "未查询到" : agentNames.getOrDefault(properyt.getAgentId(), "未查询到")); + record.put("createName", properyt.getCreateId() == null + ? "未分配" : creatorNames.getOrDefault(properyt.getCreateId(), "未分配")); + record.put("province", properyt.getProvince()); + record.put("city", properyt.getCity()); + record.put("countyseat", properyt.getCountyseat()); + record.put("address", properyt.getAddress()); + record.put("contact", properyt.getContact()); + record.put("phone", properyt.getPhone()); + record.put("createTime", properyt.getCreateTime() == null ? null : sdf.format(properyt.getCreateTime())); + records.add(record); + } + + Map 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; + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java index b7e0255..08210ad 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java @@ -2,9 +2,12 @@ package me.iiv.agent.permission; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.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.m2.WarehouseAgentAuthMapper; @@ -37,13 +40,17 @@ public class PermissionService { final AuthorityMapper authorityMapper; final AgentMapper agentMapper; final WarehouseAgentAuthMapper warehouseAgentAuthMapper; + final AgentManageDistrictAuthMapper agentManageDistrictAuthMapper; final StringRedisTemplate redisTemplate; public PermissionService(AuthorityMapper authorityMapper, AgentMapper agentMapper, - WarehouseAgentAuthMapper warehouseAgentAuthMapper, StringRedisTemplate redisTemplate) { + WarehouseAgentAuthMapper warehouseAgentAuthMapper, + AgentManageDistrictAuthMapper agentManageDistrictAuthMapper, + StringRedisTemplate redisTemplate) { this.authorityMapper = authorityMapper; this.agentMapper = agentMapper; this.warehouseAgentAuthMapper = warehouseAgentAuthMapper; + this.agentManageDistrictAuthMapper = agentManageDistrictAuthMapper; this.redisTemplate = redisTemplate; } @@ -103,4 +110,52 @@ public class PermissionService { public boolean isPropertyUser(User user) { return user.getEnterpriseType() != null && user.getEnterpriseType() == 2; } + + /** + * 代管物业 id 集合:agent_manage_district_auth 中 manage_agent_id 命中的 + * property_id 去重(老系统 getByIsAgent propertyFlag=true 分支,group by property_id)。 + */ + public List managedPropertyIds(List manageAgentIds) { + if (manageAgentIds == null || manageAgentIds.isEmpty()) { + return new ArrayList<>(); + } + List list = agentManageDistrictAuthMapper.selectList( + new LambdaQueryWrapper() + .eq(AgentManageDistrictAuth::getSoftDelete, 0) + .in(AgentManageDistrictAuth::getManageAgentId, manageAgentIds) + .groupBy(AgentManageDistrictAuth::getPropertyId)); + return list.stream().map(AgentManageDistrictAuth::getPropertyId).collect(Collectors.toList()); + } + + /** + * 物业(properyt 表)行级过滤组合,语义对应老系统 PropertyController.listByPage 使用的 + * getByIsAgent(agentFlag, agentId, wrapper, user, propertyFlag=true, className="id", redisTemplate): + *
    + *
  1. isagent=1:agent_id IN (自己+下级+warehouse_agent_auth 代管), + * 若存在代管物业则 OR id IN (agent_manage_district_auth 代管物业 id 集合);
  2. + *
  3. isagent!=1(总部):不加 agent/id 过滤;
  4. + *
  5. enterprise_type=2(物业用户):再 AND id = 用户 propertyId + * (注:老 PropertyController 未挂此限制,来自设计文档 §3.3 通用规则);
  6. + *
  7. 销售部门非负责人:再 AND create_id = 自己 + * (注:老代码 saleFlag 分支是 id IN (自建 ∪ 代管 agent 的物业), + * 此处按设计文档 §3.3"只看 create_id=自己"收窄)。
  8. + *
+ */ + public void applyPropertyRowFilter(LambdaQueryWrapper wrapper, User user) { + List agentIds = visibleAgentIds(user); + if (agentIds != null) { + List propertyIds = managedPropertyIds(agentIds); + if (!propertyIds.isEmpty()) { + wrapper.and(w -> w.in(Properyt::getAgentId, agentIds).or().in(Properyt::getId, propertyIds)); + } else { + wrapper.in(Properyt::getAgentId, agentIds); + } + } + if (isPropertyUser(user)) { + wrapper.eq(Properyt::getId, user.getPropertyId()); + } + if (isSalesNonLeader(user)) { + wrapper.eq(Properyt::getCreateId, user.getId()); + } + } }