作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
36 KiB
开发文档:客户产品清单查询页面
需求:在「客户管理」菜单下新增「客户产品清单」页面,对
customer_product_list表做查询展示。 环境:dev192.168.1.181:3306(root/kaixinjiuhao);prd 上线需替换 parent_id / authority_id 后重跑。 涉及库:xiaoqu_comples_d(菜单/字段/权限);业务库xiaoqu_franchisee_d表customer_product_list已存在,无需建表。 基础设施:Bean / Mapper / Service 也已存在(CustomerProductList/CustomerProductListMapper/CustomerProductListService及其 Impl,已被 PickupOrder / DeliveryOrder / WithdrawOrder / WarehouseHardStock 等 Controller 使用),本期只新增 Controller + DTO。
1. 数据库变更
1.1 业务表 — xiaoqu_franchisee_d(dataSource2 / m2)
customer_product_list 已存在,结构如下(不动):
-- 仅作记录,不要再次执行
CREATE TABLE `customer_product_list` (
`id` bigint(8) NOT NULL AUTO_INCREMENT,
`create_id` bigint(8) DEFAULT '0' COMMENT '创建id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_id` bigint(8) DEFAULT '0' COMMENT '修改id',
`modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`soft_delete` int(4) DEFAULT '0' COMMENT '数据状态',
`agent_id` bigint(8) DEFAULT '0' COMMENT '代理ID',
`property_id` bigint(8) DEFAULT '0' COMMENT '物业ID',
`district_id` bigint(8) DEFAULT '0' COMMENT '站点ID',
`agent_name` varchar(64) DEFAULT '' COMMENT '公司名称',
`property_name` varchar(64) DEFAULT '' COMMENT '物业名称',
`district_name` varchar(64) DEFAULT '' COMMENT '项目名称',
`materiel_definition_id`bigint(8) DEFAULT '0' COMMENT '物品ID',
`material_no` varchar(32) DEFAULT '' COMMENT '物品编号',
`materiel_full_name` varchar(256) DEFAULT '' COMMENT '物品名称',
`materiel_index_id` bigint(8) DEFAULT '0' COMMENT '索引ID',
`materiel_sort` varchar(64) DEFAULT '' COMMENT '物品分类',
`specifications` varchar(256) DEFAULT '' COMMENT '物品规格型号',
`current_inventory` int(4) DEFAULT '0' COMMENT '当前数量',
`on_the_way` int(4) DEFAULT '0' COMMENT '在途数量',
`sn_code` varchar(64) DEFAULT '' COMMENT '物品SN',
`product_sort` varchar(64) DEFAULT '' COMMENT '产品分类',
`is_back` int(4) DEFAULT '0' COMMENT '是否备用机 0:否 1:是',
`state` int(4) DEFAULT '0' COMMENT '产品状态(-1:在途;0:可用;1:待检;2:返修;3:返厂;4:在线;5:未使用;6:取回)',
`order_no` varchar(64) DEFAULT NULL COMMENT '关联工单',
`memo` varchar(64) DEFAULT NULL COMMENT '备注',
`address` varchar(256) DEFAULT '' COMMENT '地址',
`building_id` bigint(8) DEFAULT '0' COMMENT '楼id',
`floor` varchar(20) DEFAULT '' COMMENT '楼层',
`toilet_id` bigint(8) DEFAULT '0' COMMENT '厕所id',
`position_id` bigint(8) DEFAULT '0' COMMENT '厕位id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户清单';
dev 数据量:约 1275 条;state 分布 -1(996) / 0(15) / 2(1) / 4(11) / 5(252),状态 1/3/6 暂无数据。
1.2 菜单 + 字段 + 权限(权限库)
挂到「客户管理」(web_id=60) 之下,order_by=6(前面 0-5 已被占用)。
环境对应的权限 schema:
- dev →
xiaoqu_comples_d- test →
test- prod →
xiaoqu_comples_p切环境只需改第一行
USE,其余语句通用(与 messageNotice / out-certificate 同款做法)。授权策略:按 PM 决议,本页只授给
authority_id=1(系统管理员),不复用兄弟菜单的授权范围。dev/test/prod 上 authority_id=1 都对应「系统管理员」,所以三个环境 SQL 完全一致。
USE xiaoqu_comples_d; -- dev;test 改 USE test; prod 改 USE xiaoqu_comples_p;
START TRANSACTION;
-- ─────────────────────────────────────────────────────────────
-- ① webpage:菜单本体
-- ─────────────────────────────────────────────────────────────
INSERT INTO webpage
(title, param, icon, description, order_by, parent_id, `type`, `status`, `path`, is_table_show)
VALUES
('客户产品清单', '', '', '', 6, 60, 0, 1, 'customerProductList', 1);
SET @new_web_id := LAST_INSERT_ID();
SELECT @new_web_id AS new_webpage_id;
-- ─────────────────────────────────────────────────────────────
-- ② webpage_authority:菜单授权(仅系统管理员=1,与 web_id=228 站点信息统计查询一致)
-- ─────────────────────────────────────────────────────────────
INSERT INTO webpage_authority
(web_id, authority_id, title, `path`, parent_id, check_status, `type`, is_table_show, order_by)
VALUES
(@new_web_id, 1, '客户产品清单', 'customerProductList', 60, 1, 0, 1, 6);
-- ─────────────────────────────────────────────────────────────
-- ③ page_field:页面字段定义
-- class_name 全限定类路径必须与后端 DTO 一致
-- ─────────────────────────────────────────────────────────────
INSERT INTO page_field
(web_id, cn_name, en_name, class_name, field_type, order_by,
is_select, nav_select, is_table_show, param)
VALUES
-- 列表展示字段(按业务重要性排序)
(@new_web_id, '公司名称', 'agentName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 1, 1, 0, 1, ''),
(@new_web_id, '物业名称', 'propertyName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 2, 1, 0, 1, ''),
(@new_web_id, '项目名称', 'districtName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 3, 1, 0, 1, ''),
(@new_web_id, '物品编号', 'materialNo', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 4, 1, 0, 1, ''),
(@new_web_id, '物品名称', 'materielFullName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 5, 1, 1, 1, ''),
(@new_web_id, '物品分类', 'materielSort', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 6, 1, 1, 1, ''),
(@new_web_id, '产品分类', 'productSort', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 7, 1, 1, 1, ''),
(@new_web_id, '规格型号', 'specifications', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 8, 1, 0, 1, ''),
(@new_web_id, 'SN', 'snCode', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 9, 1, 1, 1, ''),
(@new_web_id, '产品状态', 'stateName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 10, 1, 0, 1, ''),
(@new_web_id, '是否备用机', 'isBackName', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 11, 1, 0, 1, ''),
(@new_web_id, '当前数量', 'currentInventory', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 12, 1, 0, 1, ''),
(@new_web_id, '在途数量', 'onTheWay', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 13, 1, 0, 1, ''),
(@new_web_id, '关联工单', 'orderNo', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 14, 1, 0, 1, ''),
(@new_web_id, '楼层', 'floor', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 15, 1, 0, 1, ''),
(@new_web_id, '地址', 'address', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 16, 1, 0, 1, ''),
(@new_web_id, '备注', 'memo', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 17, 1, 0, 1, ''),
(@new_web_id, '创建时间', 'createTime', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'date', 18, 1, 0, 1, ''),
-- 搜索区隐藏字段(用于下拉/状态枚举传 ID/枚举值)
(@new_web_id, '公司名称', 'agentId', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect', 20, 1, 1, 0, ''),
(@new_web_id, '物业名称', 'propertyId', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect', 21, 1, 1, 0, ''),
(@new_web_id, '项目名称', 'districtId', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect', 22, 1, 1, 0, ''),
(@new_web_id, '产品状态', 'state', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect', 23, 1, 1, 0, ''),
-- 公共审计字段(不展示,仅元数据完整性)
(@new_web_id, 'id', 'id', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 32, 1, 0, 0, ''),
(@new_web_id, '创建id', 'createId', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect',33, 1, 0, 0, ''),
(@new_web_id, '修改id', 'modifyId', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'inputSelect',34, 1, 0, 0, ''),
(@new_web_id, '修改时间', 'modifyTime', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'date', 35, 1, 0, 0, ''),
(@new_web_id, '数据状态', 'softDelete', 'me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput', 'string', 36, 1, 0, 0, '');
-- ─────────────────────────────────────────────────────────────
-- ④ field_webpage_authority:字段级权限(仅 authority_id=1)
-- ─────────────────────────────────────────────────────────────
INSERT INTO field_webpage_authority
(web_id, authority_id, cn_name, en_name, is_select, nav_select,
field_type, field_id, order_by, check_status, param, is_table_show)
SELECT
pf.web_id, 1 AS authority_id, pf.cn_name, pf.en_name, pf.is_select, pf.nav_select,
pf.field_type, pf.id, pf.order_by, 1, pf.param, pf.is_table_show
FROM page_field pf
WHERE pf.web_id = @new_web_id;
-- ─────────────────────────────────────────────────────────────
-- ⑤ button + button_webpage_authority:导出按钮
-- ─────────────────────────────────────────────────────────────
INSERT INTO `button`
(web_id, cn_name, en_name, icon, class_name, function_path,
`type`, `status`, order_by, theme, memo, app_en_name)
VALUES
(@new_web_id, '导出', 'export', '', '', '',
1, 1, 1, 'primary', '客户产品清单导出', '');
SET @new_export_button_id := LAST_INSERT_ID();
INSERT INTO `button_webpage_authority`
(web_id, authority_id, cn_name, en_name, button_id,
order_by, check_status, param, icon, `type`, class_name, theme, memo)
VALUES
(@new_web_id, 1, '导出', 'export', @new_export_button_id,
1, 1, '', '', 1, '', 'primary', '');
-- ─────────────────────────────────────────────────────────────
-- ⑥ 校验
-- ─────────────────────────────────────────────────────────────
SELECT id, parent_id, title, path, order_by FROM webpage WHERE id = @new_web_id;
SELECT id, authority_id, title FROM webpage_authority WHERE web_id = @new_web_id;
SELECT COUNT(*) AS field_count FROM page_field WHERE web_id = @new_web_id;
SELECT authority_id, COUNT(*) AS field_auth_count
FROM field_webpage_authority WHERE web_id = @new_web_id GROUP BY authority_id;
SELECT id, en_name, `type` FROM `button` WHERE web_id = @new_web_id;
SELECT id, en_name FROM `button_webpage_authority` WHERE web_id = @new_web_id;
COMMIT;
-- 失败请改 ROLLBACK
已执行情况(2026-05-14):
- dev (
xiaoqu_comples_d):webpage.id=387,webpage_authority=1 条(authority 1),27 字段,field_webpage_authority=27 条(27×1),button.id=790(export)- test (
test):webpage.id=389,webpage_authority=1 条(authority 1),27 字段,field_webpage_authority=27 条(27×1),button.id=791(export)
1.3 字段配置说明
| 字段 (en_name) | order_by | nav_select 搜索区 | is_table_show 列表 | field_type 控件 | 说明 |
|---|---|---|---|---|---|
| agentName | 1 | ✗ | ✓ | string | 列表「公司名称」 |
| propertyName | 2 | ✗ | ✓ | string | 列表「物业名称」 |
| districtName | 3 | ✗ | ✓ | string | 列表「项目名称」 |
| materialNo | 4 | ✗ | ✓ | string | 物品编号 |
| materielFullName | 5 | ✓ | ✓ | string | 搜索「物品名称」+ 列表(模糊) |
| materielSort | 6 | ✓ | ✓ | string | 搜索「物品分类」+ 列表(模糊) |
| productSort | 7 | ✓ | ✓ | string | 搜索「产品分类」+ 列表(模糊) |
| specifications | 8 | ✗ | ✓ | string | 规格型号 |
| snCode | 9 | ✓ | ✓ | string | 搜索 SN + 列表(模糊) |
| stateName | 10 | ✗ | ✓ | string | 产品状态文案(后端转换) |
| isBackName | 11 | ✗ | ✓ | string | 是否备用机文案(后端转换) |
| currentInventory | 12 | ✗ | ✓ | string | 当前数量 |
| onTheWay | 13 | ✗ | ✓ | string | 在途数量 |
| orderNo | 14 | ✗ | ✓ | string | 关联工单 |
| floor | 15 | ✗ | ✓ | string | 楼层 |
| address | 16 | ✗ | ✓ | string | 地址 |
| memo | 17 | ✗ | ✓ | string | 备注 |
| createTime | 18 | ✗ | ✓ | date | 创建时间 |
| agentId | 20 | ✓ | ✗ | inputSelect | 搜索「公司名称」下拉(传 ID,cn_name 同名"公司名称") |
| propertyId | 21 | ✓ | ✗ | inputSelect | 搜索「物业名称」下拉(联动 agentId) |
| districtId | 22 | ✓ | ✗ | inputSelect | 搜索「项目名称」下拉(联动 propertyId) |
| state | 23 | ✓ | ✗ | inputSelect | 搜索「产品状态」下拉 |
| id / createId / modifyId / modifyTime / softDelete | 32-36 | ✗ | ✗ | — | DTO 公共字段 |
共 18 个列表列(PM 可砍)+ 4 个搜索条件(公司/物业/项目下拉联动 + 物品名称/分类/SN/状态文本 + 产品状态枚举),列稍多但物品级表本来字段多。
2. 后端改动 — xiaoqu-franchisee
2.1 包结构与文件
xiaoqu-publicbeanandmap/... # 已存在,不动
├── beans/b2/CustomerProductList.java ✅
├── mapper/m2/CustomerProductListMapper.java ✅
└── service/s2/
├── CustomerProductListService.java ✅
└── Impl/CustomerProductListServiceImpl.java ✅
xiaoqu-franchisee/src/main/java/me/iiv/iivframework/controller/customerProduct/
├── CustomerProductListController.java 🆕 新增 @RequestMapping("/customerProductList")
├── input/CustomerProductListQueryInput.java 🆕 新增
└── output/CustomerProductListOutput.java 🆕 新增(class_name 必须与 SQL 中 page_field.class_name 一致)
xiaoqu-franchisee/src/main/java/me/iiv/iivframework/controller/export/
└── CustomerProductListExport.java 🆕 新增导出 DTO(@Excel 注解,参考 WarehouseOutExport)
2.2 API
| Method | URL | 说明 |
|---|---|---|
| POST | /customerProductList/list |
分页列表(含搜索条件) |
| POST | /customerProductList/export |
导出 Excel(不分页,按搜索条件全量导出) |
Input
@Data
public class CustomerProductListQueryInput {
private Long agentId;
private Long propertyId;
private Long districtId;
private String materielFullName; // 模糊
private String materielSort; // 模糊
private String productSort; // 模糊
private String snCode; // 模糊
private Integer state; // 精确
private Integer page = 1;
private Integer limit = 10;
private Integer rows; // 兼容前端 rows 字段
private String sidx = "id";
private String sord = "desc";
}
Output
@Data
public class CustomerProductListOutput {
private Long id;
private Long createId;
private String createTime;
private Long modifyId;
private String modifyTime;
private Integer softDelete;
private Long agentId;
private Long propertyId;
private Long districtId;
private String agentName;
private String propertyName;
private String districtName;
private Long materielDefinitionId;
private String materialNo;
private String materielFullName;
private Long materielIndexId;
private String materielSort;
private String specifications;
private Integer currentInventory;
private Integer onTheWay;
private String snCode;
private String productSort;
private Integer isBack;
private String isBackName; // 后端转:0=否 1=是
private Integer state;
private String stateName; // 后端转:-1 在途 / 0 可用 / 1 待检 / 2 返修 / 3 返厂 / 4 在线 / 5 未使用 / 6 取回
private String orderNo;
private String memo;
private String address;
private Long buildingId;
private String floor;
private Long toiletId;
private Long positionId;
}
2.3 查询逻辑
@Controller
@RequestMapping(value = "/customerProductList")
@Api(value = "客户产品清单")
public class CustomerProductListController {
@Autowired private CustomerProductListService customerProductListService;
private static final Map<Integer, String> STATE_NAMES = new HashMap<Integer, String>() {{
put(-1, "在途"); put(0, "可用"); put(1, "待检"); put(2, "返修");
put(3, "返厂"); put(4, "在线"); put(5, "未使用"); put(6, "取回");
}};
@ApiOperation(value = "客户产品清单查询列表")
@ResponseBody
@RequestMapping(value = "/list", method = RequestMethod.POST)
public BaseOutput<PageOutput<CustomerProductListOutput>> list(
HttpServletRequest request,
@RequestBody CustomerProductListQueryInput input) {
int pageNum = input.getPage() == null ? 1 : input.getPage();
int pageSize = input.getRows() != null ? input.getRows() :
(input.getLimit() != null ? input.getLimit() : 10);
Page<CustomerProductList> p = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<CustomerProductList> qw = Wrappers.<CustomerProductList>lambdaQuery()
.eq(CustomerProductList::getSoftDelete, 0)
.eq(input.getAgentId() != null, CustomerProductList::getAgentId, input.getAgentId())
.eq(input.getPropertyId() != null, CustomerProductList::getPropertyId, input.getPropertyId())
.eq(input.getDistrictId() != null, CustomerProductList::getDistrictId, input.getDistrictId())
.eq(input.getState() != null, CustomerProductList::getState, input.getState())
.like(StringUtils.isNotBlank(input.getMaterielFullName()), CustomerProductList::getMaterielFullName, input.getMaterielFullName())
.like(StringUtils.isNotBlank(input.getMaterielSort()), CustomerProductList::getMaterielSort, input.getMaterielSort())
.like(StringUtils.isNotBlank(input.getProductSort()), CustomerProductList::getProductSort, input.getProductSort())
.like(StringUtils.isNotBlank(input.getSnCode()), CustomerProductList::getSnCode, input.getSnCode());
// 简单排序:仅允许 id / create_time 两列防注入
String sidx = "id".equalsIgnoreCase(input.getSidx()) ? "id" :
"create_time".equalsIgnoreCase(input.getSidx()) ? "create_time" : "id";
boolean asc = "asc".equalsIgnoreCase(input.getSord());
qw.orderBy(true, asc, sidx);
Page<CustomerProductList> page = customerProductListService.page(p, qw);
List<CustomerProductListOutput> rows = page.getRecords().stream().map(e -> {
CustomerProductListOutput out = new CustomerProductListOutput();
BeanUtils.copyProperties(e, out);
out.setCreateTime(e.getCreateTime() == null ? null : DateUtil.format(e.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
out.setModifyTime(e.getModifyTime() == null ? null : DateUtil.format(e.getModifyTime(), "yyyy-MM-dd HH:mm:ss"));
out.setStateName(STATE_NAMES.getOrDefault(e.getState(), ""));
out.setIsBackName(e.getIsBack() != null && e.getIsBack() == 1 ? "是" : "否");
return out;
}).collect(Collectors.toList());
PageOutput<CustomerProductListOutput> resp = new PageOutput<>();
resp.setRows(rows);
resp.setRecords(page.getTotal());
return ResultResponseBeanUtil.success(resp);
}
@ApiOperation(value = "客户产品清单导出")
@PostMapping("/export")
public void export(HttpServletRequest request, HttpServletResponse response,
@RequestBody CustomerProductListQueryInput input) throws IOException {
LambdaQueryWrapper<CustomerProductList> qw = Wrappers.<CustomerProductList>lambdaQuery()
.eq(CustomerProductList::getSoftDelete, 0)
.eq(input.getAgentId() != null, CustomerProductList::getAgentId, input.getAgentId())
.eq(input.getPropertyId() != null, CustomerProductList::getPropertyId, input.getPropertyId())
.eq(input.getDistrictId() != null, CustomerProductList::getDistrictId, input.getDistrictId())
.eq(input.getState() != null, CustomerProductList::getState, input.getState())
.like(StringUtils.isNotBlank(input.getMaterielFullName()), CustomerProductList::getMaterielFullName, input.getMaterielFullName())
.like(StringUtils.isNotBlank(input.getMaterielSort()), CustomerProductList::getMaterielSort, input.getMaterielSort())
.like(StringUtils.isNotBlank(input.getProductSort()), CustomerProductList::getProductSort, input.getProductSort())
.like(StringUtils.isNotBlank(input.getSnCode()), CustomerProductList::getSnCode, input.getSnCode())
.orderByDesc(CustomerProductList::getId);
List<CustomerProductList> list = customerProductListService.list(qw);
List<CustomerProductListExport> exportList = list.stream().map(e -> {
CustomerProductListExport row = new CustomerProductListExport();
BeanUtils.copyProperties(e, row);
row.setStateName(STATE_NAMES.getOrDefault(e.getState(), ""));
row.setIsBackName(e.getIsBack() != null && e.getIsBack() == 1 ? "是" : "否");
return row;
}).collect(Collectors.toList());
ExcelUtil<CustomerProductListExport> util = new ExcelUtil<>(CustomerProductListExport.class);
util.exportExcel(response, exportList, "客户产品清单");
}
}
2.3.1 导出 DTO CustomerProductListExport
参考 WarehouseOutExport,列顺序与列表展示一致(18 列):
@ApiModel(value = "客户产品清单导出对象")
@Data
public class CustomerProductListExport {
@Excel(name = "公司名称") private String agentName;
@Excel(name = "物业名称") private String propertyName;
@Excel(name = "项目名称") private String districtName;
@Excel(name = "物品编号") private String materialNo;
@Excel(name = "物品名称") private String materielFullName;
@Excel(name = "物品分类") private String materielSort;
@Excel(name = "产品分类") private String productSort;
@Excel(name = "规格型号") private String specifications;
@Excel(name = "SN") private String snCode;
@Excel(name = "产品状态") private String stateName;
@Excel(name = "是否备用机") private String isBackName;
@Excel(name = "当前数量") private Integer currentInventory;
@Excel(name = "在途数量") private Integer onTheWay;
@Excel(name = "关联工单") private String orderNo;
@Excel(name = "楼层") private String floor;
@Excel(name = "地址") private String address;
@Excel(name = "备注") private String memo;
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
3. 前端改动 — vue-manage-system
3.1 路由
src/router/module/propertyManageRouter.js(或者放到「客户管理」对应路由文件)追加:
{
path: '/customerProductList',
name: 'customerProductList',
component: () => import('@/components/page/propertyManage/customerProductList.vue'),
meta: { title: '客户产品清单' }
}
路由文件名以现仓库实际为准(看
客户管理现有子页面propertyManage/districtForAgentManage是放哪个 router 文件里)。
3.2 API
src/api/api.js 增加块:
customerProductList: {
list: '/customerProductList/list',
export: '/customerProductList/export'
}
src/api/propertyService.js(或新建 customerProductListService.js):
export const customerProductListService = {
list(obj) {
common.setWebId(obj);
return request.post($api.customerProductList.list, obj);
},
export(obj) {
common.setWebId(obj);
return request.post($api.customerProductList.export, obj, { responseType: 'blob' }).then((res) => {
request.download('客户产品清单', res);
});
}
};
3.3 页面
src/components/page/propertyManage/customerProductList.vue,骨架直接参考 deviceManage/deviceManageSearch.vue(设备列表页,搜索区下拉级联接口模式完全一致):
- 搜索区下拉级联(沿用设备列表的 3 个下拉接口):
watcher:
dropDownService.selectAllAgent() // 公司 dropDownService.selectPropertyByAgentId({ agentId }) // 物业(依赖公司) dropDownService.selectDistrictByPropertyId({ propertyId }) // 项目(依赖物业)agentId变 → 清空 propertyId / districtId 并刷新 properties 列表;propertyId变 → 清空 districtId 并刷新 districts 列表。 - 产品状态下拉,前端硬编码 7 个状态(不含 6 取回,业务无此场景):
stateList: [ { key: -1, value: '在途' }, { key: 0, value: '可用' }, { key: 1, value: '待检' }, { key: 2, value: '返修' }, { key: 3, value: '返厂' }, { key: 4, value: '在线' }, { key: 5, value: '未使用' } ] - 列表用
standard-table+standTableMixin - 搜索区放「清空 / 搜索」 + 顶部 tableButtons 放「导出」(由按钮权限驱动,buttonsClickFun 里写
export()→ 调customerProductListService.export(this.screenForm))
模板骨架:
<template>
<div class="cust-product-list">
<div class="head" ref="topScreen">
<el-form :model="screenForm" inline label-width="100px" ref="screenForm">
<el-form-item label="公司名称">
<el-select v-model="screenForm.agentId" filterable clearable placeholder="请选择">
<el-option v-for="i in agents" :key="i.key" :label="i.value" :value="i.key" />
</el-select>
</el-form-item>
<el-form-item label="物业名称">
<el-select v-model="screenForm.propertyId" filterable clearable placeholder="请选择">
<el-option v-for="i in properties" :key="i.key" :label="i.value" :value="i.key" />
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<el-select v-model="screenForm.districtId" filterable clearable placeholder="请选择">
<el-option v-for="i in districts" :key="i.key" :label="i.value" :value="i.key" />
</el-select>
</el-form-item>
<el-form-item label="物品名称">
<el-input v-model="screenForm.materielFullName" clearable />
</el-form-item>
<el-form-item label="物品分类">
<el-input v-model="screenForm.materielSort" clearable />
</el-form-item>
<el-form-item label="产品分类">
<el-input v-model="screenForm.productSort" clearable />
</el-form-item>
<el-form-item label="SN">
<el-input v-model="screenForm.snCode" clearable />
</el-form-item>
<el-form-item label="产品状态">
<el-select v-model="screenForm.state" clearable placeholder="请选择">
<el-option v-for="i in stateList" :key="i.key" :label="i.value" :value="i.key" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="warning" @click="clearScreen">清空</el-button>
<el-button type="primary" @click="restSearch">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="main-content">
<standard-table
:tableData="tableData"
:tableHeader="tableHeader"
:tableHeight="tableHeight"
:showPagination="true"
:showScreenDetail="false"
:pageData="pageData"
showIndex
@pageChange="pageChange"
@sortChange="sortChange"
ref="mainTable"
/>
</div>
</div>
</template>
完整方法骨架(参考 deviceManageSearch.vue 660-700 行):
import { dropDownService } from '@/api/dropDownService';
watch: {
'screenForm.agentId'(val) {
this.screenForm.propertyId = '';
this.screenForm.districtId = '';
this.propertys = [];
this.districts = [];
if (val) this.fetchPropertys();
},
'screenForm.propertyId'(val) {
this.screenForm.districtId = '';
this.districts = [];
if (val) this.fetchDistricts();
}
},
methods: {
fetchAgents() {
dropDownService.selectAllAgent().then(r => { if (r.code == 0) this.agents = r.data; });
},
fetchPropertys() {
dropDownService.selectPropertyByAgentId({ agentId: this.screenForm.agentId })
.then(r => { if (r.code == 0) this.propertys = r.data; });
},
fetchDistricts() {
dropDownService.selectDistrictByPropertyId({ propertyId: this.screenForm.propertyId })
.then(r => { if (r.code == 0) this.districts = r.data; });
}
}
模板里 el-select 项的 :options 绑定到上述 agents / propertys / districts。
4. 上线执行顺序
- DEV 库:执行 §1.2(菜单 + 字段 + 权限,事务)— 业务库 §1.1 已经存在不动
- 后端:编码 →
mvn -pl xiaoqu-franchisee package→ 部署 Tomcat - 前端:编码 →
yarn build-dev→ 部署 - 冒烟:
- 管理员账号登录,左侧菜单出现「客户管理 > 客户产品清单」
- 进入页面:列表接口返回 1275 条(dev 已有数据)
- 后台「权限组管理 → 配置页面内容权限」能看到这个页面的所有字段
- 各搜索条件挨个验:公司下拉、物业下拉、项目下拉、物品名称模糊、SN 模糊、产品状态枚举
- 列表的 state / isBack 文案是否正确(看 stateName / isBackName)
- PRD 上线:在
xiaoqu_complex(注意是complex不带 s)重跑 §1.2 SQL; ⚠️ 上线前先查 PRD 库正确的「客户管理」parent_id 和 authority_id:SELECT id FROM webpage WHERE title='客户管理' AND parent_id=0; -- 用结果替换 §1.2 里的 60 SELECT id, name FROM authority WHERE name='系统管理员'; -- 用结果替换 §1.2 里的 1
5. 风险与注意
| 项 | 风险 | 处理 |
|---|---|---|
parent_id=60 硬编码 |
PRD 不一定一致 | 上线前 SELECT 取真实 id |
authority_id=1 硬编码 |
PRD 权限组结构不同 | 上线前 SELECT 取真实 id(PRD 系统管理员对应 id) |
page_field.class_name 全限定类名 |
后端 DTO 路径必须严格一致 | DTO 必须在 me.iiv.iivframework.controller.customerProduct.output 包,不要随意换名 |
| 列数过多(18 列)横向滚动 | 用户体验差 | 等 PM 评审后砍冗余列;可优先砍 building/toilet/position id 等基础字段,留业务字段 |
| 搜索区下拉级联接口 | 接口已确认(设备列表页同款) | selectAllAgent / selectPropertyByAgentId / selectDistrictByPropertyId 三个 dropDown 接口可用 |
| 导出大数据量 | 1275 → 几十万级别可能 OOM | 当前按全量查询;后续如行数 >10w 改流式导出(ExcelUtil 默认是 list 全加载到内存) |
state 文案散落两处(后端 + 前端搜索下拉) |
不一致风险 | 前端搜索区下拉的 stateList 用注释标明对应后端 STATE_NAMES 同源,改其一记得改另一处 |
| sidx 注入 | LambdaQueryWrapper 防注入 | 已白名单只允许 id / create_time |
customer_product_list 表数据量增长 |
1275→几十万级别可能慢查询 | 后续看监控加 (agent_id, soft_delete) / (sn_code) 等组合索引 |
| 备注/SN 文本可能含特殊字符 | 列表渲染 | el-table 自动转义,不用处理 |
6. PM 确认结论(已敲定,2026-05-14)
| # | 决议 |
|---|---|
| 1 | 列表列:18 列全保留。 |
| 2 | 搜索区下拉:复用「设备列表页 deviceManageSearch.vue」现成的 3 个接口:dropDownService.selectAllAgent / selectPropertyByAgentId / selectDistrictByPropertyId,含级联 watch。 |
| 3 | 产品状态枚举:列 7 个(-1 在途 / 0 可用 / 1 待检 / 2 返修 / 3 返厂 / 4 在线 / 5 未使用),不含 6 取回。 |
| 4 | 导出 Excel:要。顶部 tableButton「导出」,按当前搜索条件全量导出(参考 WarehouseOutController.allExport 模式 + @Excel 注解 DTO)。 |
| 5 | 行内"查看详情"按钮:不做。纯列表查询。 |
| 6 | 权限:仅 authority_id=1(系统管理员),同 web_id=228 站点信息统计查询。 |