Commit 382ec917 by DaiJiezhang

chore: 统一「手机号资产」文案为「手机号码管理」

纯文案替换,不含任何逻辑改动,单独成一次提交,避免 34 个文件的一行改动把
前面几次提交里真正的逻辑变更淹掉。

范围:后端提示语与注释、前端页面文案与测试断言、openspec 变更文档。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent af4adb03
...@@ -11,16 +11,16 @@ import org.springframework.web.bind.annotation.*; ...@@ -11,16 +11,16 @@ import org.springframework.web.bind.annotation.*;
public class PhoneAssetController { public class PhoneAssetController {
private final PhoneAssetService service; private final PhoneAssetService service;
private final PagePermissionService permissions; private final PagePermissionService permissions;
/** 代码作用(白话):接收手机号资产接口所需的业务服务。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):浏览器请求 -> Controller -> Service -> Mapper。 */ /** 代码作用(白话):接收手机号码管理接口所需的业务服务。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):浏览器请求 -> Controller -> Service -> Mapper。 */
/** 代码作用(白话):接收手机号业务服务和页面权限服务。关联文件:PagePermissionService.java、PhoneAssetService.java。关联逻辑(调用链/数据流):HTTP 请求 -> READ/EDIT 校验 -> 原有业务服务。 */ /** 代码作用(白话):接收手机号业务服务和页面权限服务。关联文件:PagePermissionService.java、PhoneAssetService.java。关联逻辑(调用链/数据流):HTTP 请求 -> READ/EDIT 校验 -> 原有业务服务。 */
public PhoneAssetController(PhoneAssetService service, PagePermissionService permissions){this.service=service;this.permissions=permissions;} public PhoneAssetController(PhoneAssetService service, PagePermissionService permissions){this.service=service;this.permissions=permissions;}
/** 代码作用(白话):接收浏览器分页查询并返回统一 JSON。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):GET /api/phone-assets -> Service -> ApiResponse -> Vue 表格。 */ /** 代码作用(白话):接收浏览器分页查询并返回统一 JSON。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):GET /api/phone-assets -> Service -> ApiResponse -> Vue 表格。 */
@GetMapping public ApiResponse<PhoneAssetPageResponse> page(@Valid PhoneAssetPageQuery query){permissions.require(PagePermissionService.PHONE,"READ");return ApiResponse.success(service.page(query));} @GetMapping public ApiResponse<PhoneAssetPageResponse> page(@Valid PhoneAssetPageQuery query){permissions.require(PagePermissionService.PHONE,"READ");return ApiResponse.success(service.page(query));}
/** 代码作用(白话):接收新增表单并创建手机号资产。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):POST -> DTO -> Service.create -> ApiResponse -> 弹窗。 */ /** 代码作用(白话):接收新增表单并创建手机号码管理。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):POST -> DTO -> Service.create -> ApiResponse -> 弹窗。 */
@PostMapping public ApiResponse<PhoneAssetResponse> create(@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("新增成功",service.create(request));} @PostMapping public ApiResponse<PhoneAssetResponse> create(@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("新增成功",service.create(request));}
/** 代码作用(白话):接收编辑表单并更新允许修改的字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):PUT -> Service.update -> ApiResponse -> 列表刷新。 */ /** 代码作用(白话):接收编辑表单并更新允许修改的字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):PUT -> Service.update -> ApiResponse -> 列表刷新。 */
@PutMapping("/{id}") public ApiResponse<PhoneAssetResponse> update(@PathVariable Long id,@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("编辑成功",service.update(id,request));} @PutMapping("/{id}") public ApiResponse<PhoneAssetResponse> update(@PathVariable Long id,@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("编辑成功",service.update(id,request));}
/** 代码作用(白话):软删除没有关联阻止的手机号资产。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):DELETE -> Service.softDelete -> ApiResponse -> 列表刷新。 */ /** 代码作用(白话):软删除没有关联阻止的手机号码管理。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):DELETE -> Service.softDelete -> ApiResponse -> 列表刷新。 */
@DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.PHONE,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);} @DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.PHONE,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);}
/** 代码作用(白话):按设备名称关键字搜索可关联的设备,供弹窗"关联设备"下拉使用。这里用手机号页面的 READ 权限而不是设备页的管理员权限,避免只有手机号权限的用户搜不到设备。关联文件:DeviceAssetLookupResponse.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):下拉输入 -> GET /lookups/devices -> Service 模糊查询 -> 选项列表。 */ /** 代码作用(白话):按设备名称关键字搜索可关联的设备,供弹窗"关联设备"下拉使用。这里用手机号页面的 READ 权限而不是设备页的管理员权限,避免只有手机号权限的用户搜不到设备。关联文件:DeviceAssetLookupResponse.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):下拉输入 -> GET /lookups/devices -> Service 模糊查询 -> 选项列表。 */
@GetMapping("/lookups/devices") public ApiResponse<List<DeviceAssetLookupResponse>> devices(@RequestParam(defaultValue="") String keyword){permissions.require(PagePermissionService.PHONE,"READ");return ApiResponse.success(service.searchDevices(keyword));} @GetMapping("/lookups/devices") public ApiResponse<List<DeviceAssetLookupResponse>> devices(@RequestParam(defaultValue="") String keyword){permissions.require(PagePermissionService.PHONE,"READ");return ApiResponse.success(service.searchDevices(keyword));}
......
package com.xyw.console.asset.dto; package com.xyw.console.asset.dto;
/** 文件用途(白话):承载设备资产的搜索结果,供手机号资产弹窗按设备名称选择关联设备时使用。 */ /** 文件用途(白话):承载设备资产的搜索结果,供手机号码管理弹窗按设备名称选择关联设备时使用。 */
public record DeviceAssetLookupResponse(Long id, String deviceName) {} public record DeviceAssetLookupResponse(Long id, String deviceName) {}
...@@ -3,7 +3,7 @@ package com.xyw.console.asset.dto; ...@@ -3,7 +3,7 @@ package com.xyw.console.asset.dto;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
/** 文件用途(白话):接收手机号资产列表的分页和筛选条件,不改变新增、编辑接口的数据结构。 */ /** 文件用途(白话):接收手机号码管理列表的分页和筛选条件,不改变新增、编辑接口的数据结构。 */
public record PhoneAssetPageQuery( public record PhoneAssetPageQuery(
@Min(1) Integer page, @Min(1) Integer page,
@Min(1) @Max(100) Integer size, @Min(1) @Max(100) Integer size,
......
...@@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotBlank; ...@@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
/** /**
* 代码作用(白话):限定新增和编辑时浏览器可以提交的手机号资产字段。 * 代码作用(白话):限定新增和编辑时浏览器可以提交的手机号码管理字段。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。 * 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。
* 关联逻辑(调用链/数据流):弹窗表单 -> DTO 校验 -> Service 写入允许修改的实体字段。 * 关联逻辑(调用链/数据流):弹窗表单 -> DTO 校验 -> Service 写入允许修改的实体字段。
*/ */
......
package com.xyw.console.asset.exception; package com.xyw.console.asset.exception;
/** 代码作用(白话):表示请求的手机号资产不存在或已软删除。关联文件:PhoneAssetService.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查无记录 -> 异常 -> 404 响应。 */ /** 代码作用(白话):表示请求的手机号码管理不存在或已软删除。关联文件:PhoneAssetService.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查无记录 -> 异常 -> 404 响应。 */
public class PhoneAssetNotFoundException extends RuntimeException { public PhoneAssetNotFoundException(String message){super(message);} } public class PhoneAssetNotFoundException extends RuntimeException { public PhoneAssetNotFoundException(String message){super(message);} }
\ No newline at end of file
...@@ -23,7 +23,7 @@ public class PhoneAssetService { ...@@ -23,7 +23,7 @@ public class PhoneAssetService {
private static final int DEVICE_LOOKUP_LIMIT = 20; private static final int DEVICE_LOOKUP_LIMIT = 20;
private final PhoneAssetMapper mapper; private final PhoneAssetMapper mapper;
private final AssetDeviceMapper deviceMapper; private final AssetDeviceMapper deviceMapper;
/** 代码作用(白话):接收手机号资产表和设备资产表的数据库访问入口。关联文件:PhoneAssetMapper.java、AssetDeviceMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_phone_asset / as_asset_device。 */ /** 代码作用(白话):接收手机号码管理表和设备资产表的数据库访问入口。关联文件:PhoneAssetMapper.java、AssetDeviceMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_phone_asset / as_asset_device。 */
public PhoneAssetService(PhoneAssetMapper mapper, AssetDeviceMapper deviceMapper) { this.mapper = mapper; this.deviceMapper = deviceMapper; } public PhoneAssetService(PhoneAssetMapper mapper, AssetDeviceMapper deviceMapper) { this.mapper = mapper; this.deviceMapper = deviceMapper; }
/** 代码作用(白话):按分页、手机号片段、ICCID、实名人和使用状态读取未删除资产,并补上关联设备名称。关联文件:PhoneAssetPageQuery.java、PhoneAssetController.java。关联逻辑(调用链/数据流):GET 参数 -> 查询条件 -> Mapper -> 设备名批量解析 -> Response。 */ /** 代码作用(白话):按分页、手机号片段、ICCID、实名人和使用状态读取未删除资产,并补上关联设备名称。关联文件:PhoneAssetPageQuery.java、PhoneAssetController.java。关联逻辑(调用链/数据流):GET 参数 -> 查询条件 -> Mapper -> 设备名批量解析 -> Response。 */
public PhoneAssetPageResponse page(PhoneAssetPageQuery query) { public PhoneAssetPageResponse page(PhoneAssetPageQuery query) {
...@@ -60,14 +60,14 @@ public class PhoneAssetService { ...@@ -60,14 +60,14 @@ public class PhoneAssetService {
} }
/** 代码作用(白话):判断筛选文本是否有内容,避免空字符串参与数据库筛选。关联文件:PhoneAssetPageQuery.java、PhoneAssetService.java。关联逻辑(调用链/数据流):请求参数 -> hasText -> 是否追加条件。 */ /** 代码作用(白话):判断筛选文本是否有内容,避免空字符串参与数据库筛选。关联文件:PhoneAssetPageQuery.java、PhoneAssetService.java。关联逻辑(调用链/数据流):请求参数 -> hasText -> 是否追加条件。 */
private boolean hasText(String value) { return value != null && !value.isBlank(); } private boolean hasText(String value) { return value != null && !value.isBlank(); }
/** 代码作用(白话):新增手机号资产并初始化审计字段和内部关联快照。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):POST 请求 -> DTO -> Service.create -> Mapper.insert -> as_phone_asset。 */ /** 代码作用(白话):新增手机号码管理并初始化审计字段和内部关联快照。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):POST 请求 -> DTO -> Service.create -> Mapper.insert -> as_phone_asset。 */
public PhoneAssetResponse create(PhoneAssetSaveRequest request) { PhoneAssetEntity entity=new PhoneAssetEntity(); applyEditableFields(entity,request); entity.setNumberType("SELF"); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); entity.setLinkedWecomAccounts("[]"); entity.setLinkedWechatAccounts("[]"); entity.setLinkedDouyinAccounts("[]"); entity.setLinkedDomainAccounts("[]"); entity.setLinkedMerchants("[]"); if(mapper.insert(entity)!=1) throw new IllegalStateException("手机号资产新增失败"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); } public PhoneAssetResponse create(PhoneAssetSaveRequest request) { PhoneAssetEntity entity=new PhoneAssetEntity(); applyEditableFields(entity,request); entity.setNumberType("SELF"); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); entity.setLinkedWecomAccounts("[]"); entity.setLinkedWechatAccounts("[]"); entity.setLinkedDouyinAccounts("[]"); entity.setLinkedDomainAccounts("[]"); entity.setLinkedMerchants("[]"); if(mapper.insert(entity)!=1) throw new IllegalStateException("手机号码管理新增失败"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); }
/** 代码作用(白话):更新有效资产的用户可写字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):PUT 请求 -> Service 查找 -> Mapper.updateById -> Response。 */ /** 代码作用(白话):更新有效资产的用户可写字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):PUT 请求 -> Service 查找 -> Mapper.updateById -> Response。 */
public PhoneAssetResponse update(Long id, PhoneAssetSaveRequest request) { PhoneAssetEntity entity=requireActiveEntity(id); applyEditableFields(entity,request); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); } public PhoneAssetResponse update(Long id, PhoneAssetSaveRequest request) { PhoneAssetEntity entity=requireActiveEntity(id); applyEditableFields(entity,request); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号码管理不存在或已删除"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); }
/** 代码作用(白话):将资产标记为删除,供 Controller 删除接口调用。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):DELETE 请求 -> Service -> Mapper.updateById。 */ /** 代码作用(白话):将资产标记为删除,供 Controller 删除接口调用。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):DELETE 请求 -> Service -> Mapper.updateById。 */
public void softDelete(Long id) { PhoneAssetEntity entity=requireActiveEntity(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); } public void softDelete(Long id) { PhoneAssetEntity entity=requireActiveEntity(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号码管理不存在或已删除"); }
/** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */ /** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */
private PhoneAssetEntity requireActiveEntity(Long id){ PhoneAssetEntity entity=mapper.selectOne(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime,0L).eq(PhoneAssetEntity::getId,id)); if(entity==null) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); return entity; } private PhoneAssetEntity requireActiveEntity(Long id){ PhoneAssetEntity entity=mapper.selectOne(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime,0L).eq(PhoneAssetEntity::getId,id)); if(entity==null) throw new PhoneAssetNotFoundException("手机号码管理不存在或已删除"); return entity; }
/** 代码作用(白话):把表单允许提交的字段写入实体,并统一处理手机号和默认使用状态。关联文件:PhoneAssetSaveRequest.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):弹窗表单 -> DTO -> Entity -> Mapper 保存。 */ /** 代码作用(白话):把表单允许提交的字段写入实体,并统一处理手机号和默认使用状态。关联文件:PhoneAssetSaveRequest.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):弹窗表单 -> DTO -> Entity -> Mapper 保存。 */
private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); } private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); }
/** 代码作用(白话):按已确认规则清理手机号并验证其最终格式。关联文件:PhoneAssetSaveRequest.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):前端输入 -> 去空格和 +86 -> 格式失败返回 400。 */ /** 代码作用(白话):按已确认规则清理手机号并验证其最终格式。关联文件:PhoneAssetSaveRequest.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):前端输入 -> 去空格和 +86 -> 格式失败返回 400。 */
......
...@@ -18,7 +18,7 @@ public class PagePermissionService { ...@@ -18,7 +18,7 @@ public class PagePermissionService {
public static final String PHONE = "phone-assets"; public static final String PHONE = "phone-assets";
public static final String COMPANY_PROFILE = "company-profile"; public static final String COMPANY_PROFILE = "company-profile";
public static final String ALERTS = "alerts"; public static final String ALERTS = "alerts";
private static final Map<String, String> PAGES = Map.of(OVERVIEW, "总览", DOMAIN, "域名资料", WECOM, "企微资料", PHONE, "手机号资产", COMPANY_PROFILE, "公司档案", ALERTS, "提醒中心"); private static final Map<String, String> PAGES = Map.of(OVERVIEW, "总览", DOMAIN, "域名资料", WECOM, "企微资料", PHONE, "手机号码管理", COMPANY_PROFILE, "公司档案", ALERTS, "提醒中心");
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/** 代码作用(白话):接收 JSON 工具以读取数据库权限映射;关联文件:SystemUserEntity.java。关联逻辑(调用链/数据流):page_permissions JSON -> 权限 Map -> Controller 判定。 */ /** 代码作用(白话):接收 JSON 工具以读取数据库权限映射;关联文件:SystemUserEntity.java。关联逻辑(调用链/数据流):page_permissions JSON -> 权限 Map -> Controller 判定。 */
......
...@@ -24,7 +24,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; ...@@ -24,7 +24,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
class PhoneAssetControllerTest { class PhoneAssetControllerTest {
/** /**
* 代码作用(白话):证明浏览器可以通过新增接口创建手机号资产 * 代码作用(白话):证明浏览器可以通过新增接口创建手机号码管理
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetSaveRequest.java。 * 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetSaveRequest.java。
* 关联逻辑(调用链/数据流):POST /api/phone-assets -> Controller -> Service -> Mapper -> as_phone_asset。 * 关联逻辑(调用链/数据流):POST /api/phone-assets -> Controller -> Service -> Mapper -> as_phone_asset。
*/ */
......
import { csrfHeadersFor } from '../auth/auth-api-client.js'; import { csrfHeadersFor } from '../auth/auth-api-client.js';
/** /**
* 代码作用(白话):统一发送手机号资产请求并把后端错误转换为页面可提示的文字。 * 代码作用(白话):统一发送手机号码管理请求并把后端错误转换为页面可提示的文字。
* 关联文件:PhoneAssetView.js、PhoneAssetController.java。 * 关联文件:PhoneAssetView.js、PhoneAssetController.java。
* 关联逻辑(调用链/数据流):页面事件 -> fetch -> ApiResponse -> 成功数据或 ElMessage 错误。 * 关联逻辑(调用链/数据流):页面事件 -> fetch -> ApiResponse -> 成功数据或 ElMessage 错误。
*/ */
...@@ -8,16 +8,16 @@ async function request(path, options = {}) { ...@@ -8,16 +8,16 @@ async function request(path, options = {}) {
const headers = await csrfHeadersFor(path, (options.method || 'GET').toUpperCase()); const headers = await csrfHeadersFor(path, (options.method || 'GET').toUpperCase());
const response = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...headers }, ...options }); const response = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...headers }, ...options });
const payload = await response.json(); const payload = await response.json();
if (!response.ok || payload.code !== 200) throw new Error(payload.message || '手机号资产请求失败'); if (!response.ok || payload.code !== 200) throw new Error(payload.message || '手机号码管理请求失败');
return payload.data; return payload.data;
} }
/** 代码作用(白话):按筛选条件读取手机号资产列表。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):筛选条件 -> GET -> 表格。 */ /** 代码作用(白话):按筛选条件读取手机号码管理列表。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):筛选条件 -> GET -> 表格。 */
export function listPhoneAssets(query) { const params = new URLSearchParams(); Object.entries(query).forEach(([key,value]) => { if (value !== '' && value !== null && value !== undefined && value !== 'ALL') params.set(key,value); }); return request('/api/phone-assets?' + params.toString()); } export function listPhoneAssets(query) { const params = new URLSearchParams(); Object.entries(query).forEach(([key,value]) => { if (value !== '' && value !== null && value !== undefined && value !== 'ALL') params.set(key,value); }); return request('/api/phone-assets?' + params.toString()); }
/** 代码作用(白话):提交新增表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):新增弹窗 -> POST -> 后端创建。 */ /** 代码作用(白话):提交新增表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):新增弹窗 -> POST -> 后端创建。 */
export function createPhoneAsset(form) { return request('/api/phone-assets', { method: 'POST', body: JSON.stringify(form) }); } export function createPhoneAsset(form) { return request('/api/phone-assets', { method: 'POST', body: JSON.stringify(form) }); }
/** 代码作用(白话):提交编辑表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):编辑弹窗 -> PUT -> 后端更新。 */ /** 代码作用(白话):提交编辑表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):编辑弹窗 -> PUT -> 后端更新。 */
export function updatePhoneAsset(id, form) { return request('/api/phone-assets/' + id, { method: 'PUT', body: JSON.stringify(form) }); } export function updatePhoneAsset(id, form) { return request('/api/phone-assets/' + id, { method: 'PUT', body: JSON.stringify(form) }); }
/** 代码作用(白话):确认后删除一条手机号资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):删除确认 -> DELETE -> 后端软删除。 */ /** 代码作用(白话):确认后删除一条手机号码管理。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):删除确认 -> DELETE -> 后端软删除。 */
export function deletePhoneAsset(id) { return request('/api/phone-assets/' + id, { method: 'DELETE' }); } export function deletePhoneAsset(id) { return request('/api/phone-assets/' + id, { method: 'DELETE' }); }
/** 代码作用(白话):按设备名称搜索可关联的设备资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):关联设备下拉输入 -> GET lookups/devices -> 设备选项。 */ /** 代码作用(白话):按设备名称搜索可关联的设备资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):关联设备下拉输入 -> GET lookups/devices -> 设备选项。 */
export function searchLinkableDevices(keyword) { return request('/api/phone-assets/lookups/devices?keyword=' + encodeURIComponent(keyword || '')); } export function searchLinkableDevices(keyword) { return request('/api/phone-assets/lookups/devices?keyword=' + encodeURIComponent(keyword || '')); }
...@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'; ...@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus';
import { authState } from '../auth/auth-store.js'; import { authState } from '../auth/auth-store.js';
import { createSystemUser, listLockedAccounts, listSystemUsers, resetSystemUserPassword, unlockLoginLock, updateSystemUser } from './system-user-api-client.js'; import { createSystemUser, listLockedAccounts, listSystemUsers, resetSystemUserPassword, unlockLoginLock, updateSystemUser } from './system-user-api-client.js';
const pages = [{ key: 'overview', label: '总览' }, { key: 'domain', label: '域名资料' }, { key: 'company-profile', label: '公司档案' }, { key: 'reference-wecom', label: '企微资料' }, { key: 'phone-assets', label: '手机号资产' }, { key: 'alerts', label: '提醒中心' }]; const pages = [{ key: 'overview', label: '总览' }, { key: 'domain', label: '域名资料' }, { key: 'company-profile', label: '公司档案' }, { key: 'reference-wecom', label: '企微资料' }, { key: 'phone-assets', label: '手机号码管理' }, { key: 'alerts', label: '提醒中心' }];
const roles = [{ value: 'SUPER_ADMIN', label: '超级管理员' }, { value: 'FINANCE', label: '财务' }, { value: 'HR', label: '人事' }, { value: 'OPERATIONS', label: '运营' }]; const roles = [{ value: 'SUPER_ADMIN', label: '超级管理员' }, { value: 'FINANCE', label: '财务' }, { value: 'HR', label: '人事' }, { value: 'OPERATIONS', label: '运营' }];
/** 代码作用(白话):创建五页均无权限的编辑表单初始值;关联文件:PagePermissionService.java、UserPermissionView.js。关联逻辑(调用链/数据流):新增/编辑打开 -> 本函数 -> 表单权限单选 -> JSON 提交。 */ /** 代码作用(白话):创建五页均无权限的编辑表单初始值;关联文件:PagePermissionService.java、UserPermissionView.js。关联逻辑(调用链/数据流):新增/编辑打开 -> 本函数 -> 表单权限单选 -> JSON 提交。 */
function blankForm() { return { username: '', roleCode: 'FINANCE', status: 'ACTIVE', password: '', pagePermissions: Object.fromEntries(pages.map(page => [page.key, 'NONE'])) }; } function blankForm() { return { username: '', roleCode: 'FINANCE', status: 'ACTIVE', password: '', pagePermissions: Object.fromEntries(pages.map(page => [page.key, 'NONE'])) }; }
......
import { expect, test } from './authenticated-test.js'; import { expect, test } from './authenticated-test.js';
/** /**
* 代码作用(白话):拦截手机号资产列表请求,给弹窗测试准备不依赖后端的空列表。 * 代码作用(白话):拦截手机号码管理列表请求,给弹窗测试准备不依赖后端的空列表。
* 关联文件:PhoneAssetView.js、phone-api-client.js。 * 关联文件:PhoneAssetView.js、phone-api-client.js。
* 关联逻辑(调用链/数据流):页面打开 -> listPhoneAssets -> 浏览器路由拦截 -> 空列表 -> 新增弹窗。 * 关联逻辑(调用链/数据流):页面打开 -> listPhoneAssets -> 浏览器路由拦截 -> 空列表 -> 新增弹窗。
*/ */
...@@ -19,15 +19,15 @@ async function mockPhoneAssetList(page) { ...@@ -19,15 +19,15 @@ async function mockPhoneAssetList(page) {
} }
/** /**
* 代码作用(白话):打开新增手机号资产弹窗,供多个验收场景复用。 * 代码作用(白话):打开新增手机号码管理弹窗,供多个验收场景复用。
* 关联文件:PhoneAssetView.js、phone-asset.spec.js。 * 关联文件:PhoneAssetView.js、phone-asset.spec.js。
* 关联逻辑(调用链/数据流):测试页面 -> 新增按钮 -> openCreate -> dialogVisible -> 弹窗。 * 关联逻辑(调用链/数据流):测试页面 -> 新增按钮 -> openCreate -> dialogVisible -> 弹窗。
*/ */
async function openCreateDialog(page) { async function openCreateDialog(page) {
await mockPhoneAssetList(page); await mockPhoneAssetList(page);
await page.goto('/#/phone-assets'); await page.goto('/#/phone-assets');
await page.getByRole('button', { name: '新增手机号资产' }).click(); await page.getByRole('button', { name: '新增手机号码管理' }).click();
return page.getByRole('dialog', { name: '新增手机号资产' }); return page.getByRole('dialog', { name: '新增手机号码管理' });
} }
/** /**
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
| `backend/.../controller/CompanyProfileController.java`(新增) | 暴露查询和新增 HTTP 接口,并分别执行 READ/EDIT 页面权限校验。 | | `backend/.../controller/CompanyProfileController.java`(新增) | 暴露查询和新增 HTTP 接口,并分别执行 READ/EDIT 页面权限校验。 |
| `backend/.../auth/PagePermissionService.java` | 注册公司档案权限键,令鉴权接口、权限配置和前端菜单共用同一份权限数据。 | | `backend/.../auth/PagePermissionService.java` | 注册公司档案权限键,令鉴权接口、权限配置和前端菜单共用同一份权限数据。 |
| `frontend/src/modules/company-profile/company-profile-api-client.js`(新增) | 集中请求公司档案分页和创建接口并统一处理接口信封与 CSRF。 | | `frontend/src/modules/company-profile/company-profile-api-client.js`(新增) | 集中请求公司档案分页和创建接口并统一处理接口信封与 CSRF。 |
| `frontend/src/modules/company-profile/CompanyProfileView.js`(新增) | 提供搜索、分页、全字段表格展示及复用手机号资产布局的新增弹窗。 | | `frontend/src/modules/company-profile/CompanyProfileView.js`(新增) | 提供搜索、分页、全字段表格展示及复用手机号码管理布局的新增弹窗。 |
| `frontend/src/router/index.js` | 增加公司档案 Hash 路由和对应权限元数据。 | | `frontend/src/router/index.js` | 增加公司档案 Hash 路由和对应权限元数据。 |
| `frontend/src/App.js` | 在现有导航中新增“公司档案”菜单项,并沿用既有权限隐藏规则。 | | `frontend/src/App.js` | 在现有导航中新增“公司档案”菜单项,并沿用既有权限隐藏规则。 |
| `frontend/src/modules/system-user/UserPermissionView.js` | 将公司档案加入既有账号逐页权限配置,使管理员可以授予 READ 或 EDIT。 | | `frontend/src/modules/system-user/UserPermissionView.js` | 将公司档案加入既有账号逐页权限配置,使管理员可以授予 READ 或 EDIT。 |
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
- 支持公司名称、公司简称、统一社会信用代码、联系人和联系方式的关键词搜索,并默认按 ID 倒序分页。 - 支持公司名称、公司简称、统一社会信用代码、联系人和联系方式的关键词搜索,并默认按 ID 倒序分页。
- 新增“公司档案”菜单和路由,前端守卫、菜单可见性、后端接口使用统一的 `company-profile` 权限键。 - 新增“公司档案”菜单和路由,前端守卫、菜单可见性、后端接口使用统一的 `company-profile` 权限键。
- 企微注册主体使用可搜索但不可自由输入/新建的选择控件,选择值为 `as_company_profile.id`,优先显示 `short_name`,简称为空时回退公司名称。 - 企微注册主体使用可搜索但不可自由输入/新建的选择控件,选择值为 `as_company_profile.id`,优先显示 `short_name`,简称为空时回退公司名称。
- 提供复用手机号资产新增样式的公司档案新增弹窗;公司名称必填,其余字段可空,审计字段由后端自动设置。 - 提供复用手机号码管理新增样式的公司档案新增弹窗;公司名称必填,其余字段可空,审计字段由后端自动设置。
**Non-Goals:** **Non-Goals:**
...@@ -63,9 +63,9 @@ ...@@ -63,9 +63,9 @@
`PagePermissionService` 注册 `company-profile`,查询接口要求 READ(可查看)权限,创建接口要求 EDIT(可新增)权限;前端路由 meta(路由附加信息)使用同一键,菜单和新增按钮也依据该键显示。管理员默认获得 EDIT,普通用户的 READ 只能查看。 `PagePermissionService` 注册 `company-profile`,查询接口要求 READ(可查看)权限,创建接口要求 EDIT(可新增)权限;前端路由 meta(路由附加信息)使用同一键,菜单和新增按钮也依据该键显示。管理员默认获得 EDIT,普通用户的 READ 只能查看。
### 4. 新增弹窗复用手机号资产布局 ### 4. 新增弹窗复用手机号码管理布局
公司档案页采用手机号资产已有的 Element Plus 弹窗宽度、表单间距和底部操作按钮样式,但保持独立表单状态和提交函数。这样视觉体验一致,字段和保存规则仍由公司档案模块独立维护。 公司档案页采用手机号码管理已有的 Element Plus 弹窗宽度、表单间距和底部操作按钮样式,但保持独立表单状态和提交函数。这样视觉体验一致,字段和保存规则仍由公司档案模块独立维护。
替代方案是复用企微资料权限。 不采用:公司档案是独立菜单,复用会造成用户可看企微即自动可看公司主体资料,权限边界不清。 替代方案是复用企微资料权限。 不采用:公司档案是独立菜单,复用会造成用户可看企微即自动可看公司主体资料,权限边界不清。
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
### 7. 企业微信新增弹窗复用统一资产容器 ### 7. 企业微信新增弹窗复用统一资产容器
企业微信新增表单改用手机号资产已存在的 `phone-asset-modal``phone-asset-modal__form``phone-asset-modal__form-row` 样式类,并在弹窗打开后将正文滚动位置复位到顶部。统一资产容器已将标题、正文和底部操作区分层:正文过高时只滚动正文,用户始终能看到标题和取消/保存按钮。字段控件与保存函数保持原实现,避免改变企业微信创建数据流。 企业微信新增表单改用手机号码管理已存在的 `phone-asset-modal``phone-asset-modal__form``phone-asset-modal__form-row` 样式类,并在弹窗打开后将正文滚动位置复位到顶部。统一资产容器已将标题、正文和底部操作区分层:正文过高时只滚动正文,用户始终能看到标题和取消/保存按钮。字段控件与保存函数保持原实现,避免改变企业微信创建数据流。
## Risks / Trade-offs ## Risks / Trade-offs
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
### Requirement: 公司档案新增弹窗 ### Requirement: 公司档案新增弹窗
系统 SHALL 仅向具有 `company-profile` EDIT 权限的用户显示“新增公司档案”按钮。弹窗 MUST 复用手机号资产新增弹窗的布局和操作样式,标记公司名称为必填,其余五个业务字段为可选;保存成功后 MUST 关闭弹窗并刷新公司档案列表。页面仍不得展示内部 ID。 系统 SHALL 仅向具有 `company-profile` EDIT 权限的用户显示“新增公司档案”按钮。弹窗 MUST 复用手机号码管理新增弹窗的布局和操作样式,标记公司名称为必填,其余五个业务字段为可选;保存成功后 MUST 关闭弹窗并刷新公司档案列表。页面仍不得展示内部 ID。
#### Scenario: 编辑权限用户保存新增弹窗 #### Scenario: 编辑权限用户保存新增弹窗
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
### Requirement: 企业微信新增弹窗复用资产样式并限制内容区滚动 ### Requirement: 企业微信新增弹窗复用资产样式并限制内容区滚动
系统 SHALL 使用手机号资产新增弹窗的统一外观展示企业微信新增表单。弹窗 MUST 固定标题与底部操作区;当表单内容超过可用高度时,MUST 只让弹窗正文区域纵向滚动。此视觉调整不得改变企业微信字段、注册主体远程搜索选择、手机号校验或 POST 请求内容。 系统 SHALL 使用手机号码管理新增弹窗的统一外观展示企业微信新增表单。弹窗 MUST 固定标题与底部操作区;当表单内容超过可用高度时,MUST 只让弹窗正文区域纵向滚动。此视觉调整不得改变企业微信字段、注册主体远程搜索选择、手机号校验或 POST 请求内容。
#### Scenario: 企业微信表单内容超过可用高度 #### Scenario: 企业微信表单内容超过可用高度
......
...@@ -32,10 +32,10 @@ ...@@ -32,10 +32,10 @@
- [x] 5.1 新增 `CompanyProfileSaveRequest`,为公司名称必填和其余字段可选定义校验;每个方法写新手注释。 - [x] 5.1 新增 `CompanyProfileSaveRequest`,为公司名称必填和其余字段可选定义校验;每个方法写新手注释。
- [x] 5.2 为公司档案 Service/Controller 的创建规则先新增失败测试,覆盖空公司名称、审计字段自动写入、EDIT 成功和 READ 拒绝;测试辅助方法写新手注释。 - [x] 5.2 为公司档案 Service/Controller 的创建规则先新增失败测试,覆盖空公司名称、审计字段自动写入、EDIT 成功和 READ 拒绝;测试辅助方法写新手注释。
- [x] 5.3 修改 `CompanyProfileService``CompanyProfileController`,实现创建方法、字段规范化、审计字段初始化与 EDIT 权限校验;所有新增/修改方法写新手注释。 - [x] 5.3 修改 `CompanyProfileService``CompanyProfileController`,实现创建方法、字段规范化、审计字段初始化与 EDIT 权限校验;所有新增/修改方法写新手注释。
- [x] 5.4 修改公司档案 API 客户端和页面,增加可创建权限、弹窗状态、表单重置、必填提示与保存方法;每个新增/修改方法写新手注释,并复用手机号资产新增弹窗的样式。 - [x] 5.4 修改公司档案 API 客户端和页面,增加可创建权限、弹窗状态、表单重置、必填提示与保存方法;每个新增/修改方法写新手注释,并复用手机号码管理新增弹窗的样式。
- [x] 5.5 更新前端浏览器测试,覆盖新增按钮权限、公司名称必填、请求正文、成功提示、关闭弹窗与列表刷新;运行后端测试、前端构建和全量浏览器测试。 - [x] 5.5 更新前端浏览器测试,覆盖新增按钮权限、公司名称必填、请求正文、成功提示、关闭弹窗与列表刷新;运行后端测试、前端构建和全量浏览器测试。
## 6. 企业微信新增弹窗统一化 ## 6. 企业微信新增弹窗统一化
- [x] 6.1 修改 `WecomAccountView.js`,复用手机号资产的弹窗、表单行和内容区滚动样式;新增弹窗打开后滚动复位方法并写新手注释,保留原有字段、搜索和保存逻辑。 - [x] 6.1 修改 `WecomAccountView.js`,复用手机号码管理的弹窗、表单行和内容区滚动样式;新增弹窗打开后滚动复位方法并写新手注释,保留原有字段、搜索和保存逻辑。
- [x] 6.2 更新 `wecom-account.spec.js`,覆盖统一弹窗类、超高内容仅在正文区域滚动和原有新增请求;运行前端构建和全量浏览器测试。 - [x] 6.2 更新 `wecom-account.spec.js`,覆盖统一弹窗类、超高内容仅在正文区域滚动和原有新增请求;运行前端构建和全量浏览器测试。
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
`xyw_data_test.as_phone_asset``PhoneAssetEntity``PhoneAssetMapper` 已存在,但没有 Controller、Service、DTO、分页配置或可操作的前端页面。当前前端只在 `/reference/phone` 保留静态参考;根导航中的“手机号卡(参考)”不连接任何新表接口。 `xyw_data_test.as_phone_asset``PhoneAssetEntity``PhoneAssetMapper` 已存在,但没有 Controller、Service、DTO、分页配置或可操作的前端页面。当前前端只在 `/reference/phone` 保留静态参考;根导航中的“手机号卡(参考)”不连接任何新表接口。
本变更是前后端联动:以已有 `as_phone_asset` 为唯一持久化来源,补齐手机号资产独立维护闭环。它不假设其他资产表的业务 Service 已完成,因此不能把手机号的关联快照当成用户可编辑的关系来源。 本变更是前后端联动:以已有 `as_phone_asset` 为唯一持久化来源,补齐手机号码管理独立维护闭环。它不假设其他资产表的业务 Service 已完成,因此不能把手机号的关联快照当成用户可编辑的关系来源。
### 范围文件用途注释表 ### 范围文件用途注释表
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
| `backend/.../asset/exception/*.java`(新增) | 业务异常与统一返回 | 将不存在、重复、请求校验失败稳定映射为 404/409/400 | Service/Validator → Exception handler → `ApiResponse.error` | | `backend/.../asset/exception/*.java`(新增) | 业务异常与统一返回 | 将不存在、重复、请求校验失败稳定映射为 404/409/400 | Service/Validator → Exception handler → `ApiResponse.error` |
| `backend/src/test/.../asset/**/*.java`(新增) | 后端自动化测试 | 覆盖分页、重复、软删除、输入校验和错误返回 | Service/Controller → MockMapper/MockMvc | | `backend/src/test/.../asset/**/*.java`(新增) | 后端自动化测试 | 覆盖分页、重复、软删除、输入校验和错误返回 | Service/Controller → MockMapper/MockMvc |
| `frontend/src/modules/phone/phone-api-client.js`(新增) | 浏览器 API 客户端 | 组装请求、解析统一返回体、抛出可显示错误 | 视图事件 → fetch → 后端 API | | `frontend/src/modules/phone/phone-api-client.js`(新增) | 浏览器 API 客户端 | 组装请求、解析统一返回体、抛出可显示错误 | 视图事件 → fetch → 后端 API |
| `frontend/src/modules/phone/PhoneAssetView.js`(新增) | 手机号资产工作页面 | 筛选、表格、分页、编辑弹窗、删除确认和详情 | 路由 → View → API client → 页面状态 | | `frontend/src/modules/phone/PhoneAssetView.js`(新增) | 手机号码管理工作页面 | 筛选、表格、分页、编辑弹窗、删除确认和详情 | 路由 → View → API client → 页面状态 |
| `frontend/src/router/index.js`(修改) | 前端路由 | 注册 `/phone-assets`;保留 `/reference/phone` 静态回退 | URL → PhoneAssetView | | `frontend/src/router/index.js`(修改) | 前端路由 | 注册 `/phone-assets`;保留 `/reference/phone` 静态回退 | URL → PhoneAssetView |
| `frontend/src/App.js`(修改) | 导航壳 | 将主导航指向可操作手机号资产页 | 导航点击 → RouterLink → 路由 | | `frontend/src/App.js`(修改) | 导航壳 | 将主导航指向可操作手机号码管理页 | 导航点击 → RouterLink → 路由 |
| `frontend/src/styles/app.css`(修改) | 共享样式 | 为工作页筛选区、表格、详情和窄屏布局提供样式 | View DOM → CSS | | `frontend/src/styles/app.css`(修改) | 共享样式 | 为工作页筛选区、表格、详情和窄屏布局提供样式 | View DOM → CSS |
| `frontend/tests/phone-asset.spec.js`(新增) | 前端端到端测试 | 以 mock API 验证页面加载、保存、删除和错误展示 | Playwright → View → mock `/api/phone-assets` | | `frontend/tests/phone-asset.spec.js`(新增) | 前端端到端测试 | 以 mock API 验证页面加载、保存、删除和错误展示 | Playwright → View → mock `/api/phone-assets` |
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
**Goals:** **Goals:**
- 实现手机号资产的分页查询、单条查询、新增、编辑与软删除,所有有效记录读取均固定 `delete_time = 0` - 实现手机号码管理的分页查询、单条查询、新增、编辑与软删除,所有有效记录读取均固定 `delete_time = 0`
- 保存前去除手机号的首尾空格和 `+86` 前缀,并校验最终值为 11 位数字;手机号重复必须反馈为冲突,不覆盖原记录。 - 保存前去除手机号的首尾空格和 `+86` 前缀,并校验最终值为 11 位数字;手机号重复必须反馈为冲突,不覆盖原记录。
- 使用固定下拉选项:卡类型为中国移动/中国电信/中国联通/中国广电/虚拟号码/空串,管理类型为自有/租用/代运营/空值,处置状态为正常使用/闲置/停机/已注销且必填、默认正常使用。 - 使用固定下拉选项:卡类型为中国移动/中国电信/中国联通/中国广电/虚拟号码/空串,管理类型为自有/租用/代运营/空值,处置状态为正常使用/闲置/停机/已注销且必填、默认正常使用。
- 新建记录将五个 JSON 关联快照显式初始化为 `[]`,并只读返回数组形式给前端。 - 新建记录将五个 JSON 关联快照显式初始化为 `[]`,并只读返回数组形式给前端。
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
### 4. 前端采用模块化 JS 视图与 Element Plus 原生组件 ### 4. 前端采用模块化 JS 视图与 Element Plus 原生组件
新增 `PhoneAssetView.js``phone-api-client.js`,延续当前非 SFC 的 Vue JavaScript 写法。页面由筛选栏、分页表格、右侧详情抽屉、同一新增/编辑弹窗和删除二次确认组成;所有 API 错误用 `ElMessage` 展示,保存成功后刷新当前筛选条件。路由新增 `/phone-assets`,导航文案从“手机号卡(参考)”调整为“手机号资产”;`/reference/phone` 继续保留。 新增 `PhoneAssetView.js``phone-api-client.js`,延续当前非 SFC 的 Vue JavaScript 写法。页面由筛选栏、分页表格、右侧详情抽屉、同一新增/编辑弹窗和删除二次确认组成;所有 API 错误用 `ElMessage` 展示,保存成功后刷新当前筛选条件。路由新增 `/phone-assets`,导航文案从“手机号卡(参考)”调整为“手机号码管理”;`/reference/phone` 继续保留。
备选为直接替换 `LegacyReferenceView`。该组件同时承载静态回退语义,直接替换会损失安全参考入口,故不采用。 备选为直接替换 `LegacyReferenceView`。该组件同时承载静态回退语义,直接替换会损失安全参考入口,故不采用。
......
## Why ## Why
`xyw_data_test` 已完成 `as_phone_asset` 表创建,后端也已有对应的 Entity 与 Mapper,但系统尚不能查询或维护手机号资产;前端仅保留不可操作的旧界面参考。先完成手机号资产的独立闭环,可以验证新表、MyBatis-Plus 和新 Vue 工作区的整合方式,并为后续企微、微信、抖音、域名与商户资产复用。 `xyw_data_test` 已完成 `as_phone_asset` 表创建,后端也已有对应的 Entity 与 Mapper,但系统尚不能查询或维护手机号码管理;前端仅保留不可操作的旧界面参考。先完成手机号码管理的独立闭环,可以验证新表、MyBatis-Plus 和新 Vue 工作区的整合方式,并为后续企微、微信、抖音、域名与商户资产复用。
## What Changes ## What Changes
- 新增以 `as_phone_asset` 为唯一写入目标的手机号资产 API:分页查询、单条查询、新增、编辑和软删除。 - 新增以 `as_phone_asset` 为唯一写入目标的手机号码管理 API:分页查询、单条查询、新增、编辑和软删除。
- 新增手机号规范化与唯一性保护:保存前去除首尾空格和 `+86` 前缀,最终必须为 11 位数字。 - 新增手机号规范化与唯一性保护:保存前去除首尾空格和 `+86` 前缀,最终必须为 11 位数字。
- 将卡类型、管理类型和处置状态做成已确认选项的下拉框;处置状态必填且默认“正常使用”,其余非必填。 - 将卡类型、管理类型和处置状态做成已确认选项的下拉框;处置状态必填且默认“正常使用”,其余非必填。
- 由后端在新增时初始化五个关联快照字段为 JSON 空数组;本期不自动写入或同步其他资产表的数据。 - 由后端在新增时初始化五个关联快照字段为 JSON 空数组;本期不自动写入或同步其他资产表的数据。
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
### New Capabilities ### New Capabilities
- `phone-asset-api`: 以软删除和字段校验保护 `as_phone_asset` 的手机号资产 REST API。 - `phone-asset-api`: 以软删除和字段校验保护 `as_phone_asset` 的手机号码管理 REST API。
- `phone-asset-workspace`: 用于浏览和维护手机号资产的 Vue 工作页面,以及与 API 的交互状态。 - `phone-asset-workspace`: 用于浏览和维护手机号码管理的 Vue 工作页面,以及与 API 的交互状态。
### Modified Capabilities ### Modified Capabilities
...@@ -25,5 +25,5 @@ ...@@ -25,5 +25,5 @@
## Impact ## Impact
- 后端新增 `asset` 模块的 Controller、Service、请求/响应 DTO、异常处理及对应测试;复用既有 `PhoneAssetEntity``PhoneAssetMapper``ApiResponse`,不改表结构或 Mapper XML。 - 后端新增 `asset` 模块的 Controller、Service、请求/响应 DTO、异常处理及对应测试;复用既有 `PhoneAssetEntity``PhoneAssetMapper``ApiResponse`,不改表结构或 Mapper XML。
- 前端新增手机号资产页面和 API 客户端,修改路由、导航与共享样式,并新增端到端测试。 - 前端新增手机号码管理页面和 API 客户端,修改路由、导航与共享样式,并新增端到端测试。
- 新增 `/api/phone-assets` 系列接口;不修改既有接口、DTO、数据库结构、认证或配置,也不新增依赖。本期不做登录权限和关联快照自动同步,`deviceId` 可为空且不校验设备是否存在。 - 新增 `/api/phone-assets` 系列接口;不修改既有接口、DTO、数据库结构、认证或配置,也不新增依赖。本期不做登录权限和关联快照自动同步,`deviceId` 可为空且不校验设备是否存在。
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
The system SHALL provide a routed phone-asset workspace at `/phone-assets` and SHALL retain `/reference/phone` as a non-operational static reference route. The system SHALL provide a routed phone-asset workspace at `/phone-assets` and SHALL retain `/reference/phone` as a non-operational static reference route.
#### Scenario: User opens the main workspace #### Scenario: User opens the main workspace
- **WHEN** a user selects “手机号资产” from the main navigation - **WHEN** a user selects “手机号码管理” from the main navigation
- **THEN** the router opens `/phone-assets` and the page requests the first asset page - **THEN** the router opens `/phone-assets` and the page requests the first asset page
#### Scenario: User opens the legacy route #### Scenario: User opens the legacy route
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
- [ ] 1.3 新增“未找到”和“手机号冲突”业务异常及统一异常处理;为 `handleValidation()``handleNotFound()``handleConflict()` 添加新手注释,分别说明校验/Service 异常 → `ApiResponse.error` → 前端错误提示的调用链。 - [ ] 1.3 新增“未找到”和“手机号冲突”业务异常及统一异常处理;为 `handleValidation()``handleNotFound()``handleConflict()` 添加新手注释,分别说明校验/Service 异常 → `ApiResponse.error` → 前端错误提示的调用链。
- [ ] 1.4 为 DTO 校验、分页默认值和 400/404/409 响应编写后端测试,验证失败请求不写入 `as_phone_asset` - [ ] 1.4 为 DTO 校验、分页默认值和 400/404/409 响应编写后端测试,验证失败请求不写入 `as_phone_asset`
## 2. 后端手机号资产服务与接口 ## 2. 后端手机号码管理服务与接口
- [ ] 2.1 新增 `PhoneAssetService`,实现 `page()``getActiveById()``create()``update()``softDelete()`;每个方法添加新手注释,至少说明各自的白话作用、关联 Controller/Mapper/DTO 和“HTTP 请求 → Service → `PhoneAssetMapper``as_phone_asset`”数据流。 - [ ] 2.1 新增 `PhoneAssetService`,实现 `page()``getActiveById()``create()``update()``softDelete()`;每个方法添加新手注释,至少说明各自的白话作用、关联 Controller/Mapper/DTO 和“HTTP 请求 → Service → `PhoneAssetMapper``as_phone_asset`”数据流。
- [ ] 2.2 在 `PhoneAssetService` 中实现 `buildActiveQuery()``ensurePhoneNumberAvailable()``applyEditableFields()``toResponse()``parseRelationIds()`;每个方法添加新手注释,说明有效记录过滤、唯一性保护、可写字段边界、Entity→Response 转换和 JSON 快照解析各自的关联文件与数据流。 - [ ] 2.2 在 `PhoneAssetService` 中实现 `buildActiveQuery()``ensurePhoneNumberAvailable()``applyEditableFields()``toResponse()``parseRelationIds()`;每个方法添加新手注释,说明有效记录过滤、唯一性保护、可写字段边界、Entity→Response 转换和 JSON 快照解析各自的关联文件与数据流。
...@@ -19,13 +19,13 @@ ...@@ -19,13 +19,13 @@
- [ ] 2.4 新增 `PhoneAssetController``page()``getById()``create()``update()``delete()`;每个方法添加新手注释,说明 HTTP 路径、关联 Service/DTO、以及请求 → `ApiResponse` → 浏览器客户端的数据流。 - [ ] 2.4 新增 `PhoneAssetController``page()``getById()``create()``update()``delete()`;每个方法添加新手注释,说明 HTTP 路径、关联 Service/DTO、以及请求 → `ApiResponse` → 浏览器客户端的数据流。
- [ ] 2.5 为服务与 Controller 编写测试:默认分页、组合筛选、精确手机号、创建、重复手机号、编辑冲突、软删除后不可读、软删除后可重建、关联字段不可由请求覆盖。 - [ ] 2.5 为服务与 Controller 编写测试:默认分页、组合筛选、精确手机号、创建、重复手机号、编辑冲突、软删除后不可读、软删除后可重建、关联字段不可由请求覆盖。
## 3. 前端手机号资产工作页 ## 3. 前端手机号码管理工作页
- [ ] 3.1 新增 `phone-api-client.js``request()``listPhoneAssets()``getPhoneAsset()``createPhoneAsset()``updatePhoneAsset()``deletePhoneAsset()`;每个方法添加新手注释,说明浏览器请求、关联 `PhoneAssetController`、以及统一响应解析/异常消息的数据流。 - [ ] 3.1 新增 `phone-api-client.js``request()``listPhoneAssets()``getPhoneAsset()``createPhoneAsset()``updatePhoneAsset()``deletePhoneAsset()`;每个方法添加新手注释,说明浏览器请求、关联 `PhoneAssetController`、以及统一响应解析/异常消息的数据流。
- [ ] 3.2 新增 `PhoneAssetView.js``setup()``loadPage()``submitSearch()``resetSearch()``openCreate()``openEdit()``submitForm()``confirmDelete()``openDetails()`;每个方法或含业务逻辑回调添加新手注释,说明页面事件、关联 API 客户端/Element Plus、以及“用户操作 → API → 列表或弹窗状态”的消息链。 - [ ] 3.2 新增 `PhoneAssetView.js``setup()``loadPage()``submitSearch()``resetSearch()``openCreate()``openEdit()``submitForm()``confirmDelete()``openDetails()`;每个方法或含业务逻辑回调添加新手注释,说明页面事件、关联 API 客户端/Element Plus、以及“用户操作 → API → 列表或弹窗状态”的消息链。
- [ ] 3.3 实现筛选栏、分页表格、新增/编辑共用表单、删除二次确认和只读关联快照详情;保存失败时保留弹窗与表单数据,成功后仅刷新当前筛选列表。 - [ ] 3.3 实现筛选栏、分页表格、新增/编辑共用表单、删除二次确认和只读关联快照详情;保存失败时保留弹窗与表单数据,成功后仅刷新当前筛选列表。
- [ ] 3.4 修改 `frontend/src/router/index.js` 注册 `/phone-assets` 并保留 `/reference/phone`;为 `createPlaceholderView()` 重新核对并补充其新手注释,说明占位页面与新路由的边界,避免旧入口误接新 API。 - [ ] 3.4 修改 `frontend/src/router/index.js` 注册 `/phone-assets` 并保留 `/reference/phone`;为 `createPlaceholderView()` 重新核对并补充其新手注释,说明占位页面与新路由的边界,避免旧入口误接新 API。
- [ ] 3.5 修改 `frontend/src/App.js` 的导航模板,将“手机号卡(参考)”改为“手机号资产”并指向 `/phone-assets`;补充组件模板相关注释,说明 RouterLink → RouterView 的导航链。 - [ ] 3.5 修改 `frontend/src/App.js` 的导航模板,将“手机号卡(参考)”改为“手机号码管理”并指向 `/phone-assets`;补充组件模板相关注释,说明 RouterLink → RouterView 的导航链。
- [ ] 3.6 修改 `frontend/src/styles/app.css`,为筛选、表格、抽屉、弹窗和窄屏布局增加与现有后台一致的样式;不改变其他参考页面的可读性。 - [ ] 3.6 修改 `frontend/src/styles/app.css`,为筛选、表格、抽屉、弹窗和窄屏布局增加与现有后台一致的样式;不改变其他参考页面的可读性。
## 4. 验证与交付 ## 4. 验证与交付
......
## Context ## Context
企业微信资产目前只能分页查询;手机号资产仅能在自己的页面创建。`as_phone_asset` 尚未保存号码类型和外部来源,企业微信列表也不能按注册手机号筛选。仓库没有可执行的数据库迁移目录,因此数据库变更只作为受控 SQL 交付,绝不在应用启动或本次任务中直接执行。 企业微信资产目前只能分页查询;手机号码管理仅能在自己的页面创建。`as_phone_asset` 尚未保存号码类型和外部来源,企业微信列表也不能按注册手机号筛选。仓库没有可执行的数据库迁移目录,因此数据库变更只作为受控 SQL 交付,绝不在应用启动或本次任务中直接执行。
## Goals / Non-Goals ## Goals / Non-Goals
**Goals:** **Goals:**
- 提供企业微信资产新增表单、关联查询和手机号自动创建。 - 提供企业微信资产新增表单、关联查询和手机号自动创建。
- 在手机号资产中长期保存并显示“自有号码”或“外部号码”。 - 在手机号码管理中长期保存并显示“自有号码”或“外部号码”。
- 让外部号码链接到对应来源资产列表,并按手机号资产筛选。 - 让外部号码链接到对应来源资产列表,并按手机号码管理筛选。
- 用可复用的来源类型字段为后续微信、抖音等页面预留接入点。 - 用可复用的来源类型字段为后续微信、抖音等页面预留接入点。
**Non-Goals:** **Non-Goals:**
...@@ -21,11 +21,11 @@ ...@@ -21,11 +21,11 @@
### 手机号号码类型与来源分开保存 ### 手机号号码类型与来源分开保存
手机号资产新增 `number_type``source_asset_type``source_asset_id``number_type` 只表示号码归属:手机号资产页直接创建为 `SELF`(自有号码),其他资产流程自动创建为 `EXTERNAL`(外部号码);来源字段记录创建它的资产类型和主键。相比把“已有/新建”写入手机号资产,这能避免把一次关联动作误当成号码自身属性。 手机号码管理新增 `number_type``source_asset_type``source_asset_id``number_type` 只表示号码归属:手机号码管理页直接创建为 `SELF`(自有号码),其他资产流程自动创建为 `EXTERNAL`(外部号码);来源字段记录创建它的资产类型和主键。相比把“已有/新建”写入手机号码管理,这能避免把一次关联动作误当成号码自身属性。
### 企业微信关联方式单独展示 ### 企业微信关联方式单独展示
企业微信资产新增 `phone_link_mode`。选择已有手机号资产时保存 `EXISTING`(已有号码);自动创建手机号资产时保存 `CREATED`(新建号码)。这与手机号资产的号码类型是两条不同维度的信息。 企业微信资产新增 `phone_link_mode`。选择已有手机号码管理时保存 `EXISTING`(已有号码);自动创建手机号码管理时保存 `CREATED`(新建号码)。这与手机号码管理的号码类型是两条不同维度的信息。
### 原子保存 ### 原子保存
...@@ -37,15 +37,15 @@ ...@@ -37,15 +37,15 @@
## Risks / Trade-offs ## Risks / Trade-offs
- [历史手机号没有来源] → 数据库交付将历史记录初始化为 `SELF`,与“非手机号资产创建才是外部号码”的规则一致。 - [历史手机号没有来源] → 数据库交付将历史记录初始化为 `SELF`,与“非手机号码管理创建才是外部号码”的规则一致。
- [并发提交同一新号码] → 复用手机号资产表既有“号码 + 删除状态”的唯一约束,并将重复键错误转换为重新读取已有号码。 - [并发提交同一新号码] → 复用手机号码管理表既有“号码 + 删除状态”的唯一约束,并将重复键错误转换为重新读取已有号码。
- [未来来源页面尚未实现] → 当前仅企业微信具备完整跳转;来源类型字段和前端映射为未来页面预留扩展点。 - [未来来源页面尚未实现] → 当前仅企业微信具备完整跳转;来源类型字段和前端映射为未来页面预留扩展点。
- [数据库脚本不在仓库] → 交付明确的字段与回填要求,实际执行必须获得目标数据库和执行窗口的单独授权。 - [数据库脚本不在仓库] → 交付明确的字段与回填要求,实际执行必须获得目标数据库和执行窗口的单独授权。
## Migration Plan ## Migration Plan
1. 在受控数据库变更中为手机号资产增加号码类型、来源资产类型、来源资产 ID,为企业微信资产增加手机号关联方式。 1. 在受控数据库变更中为手机号码管理增加号码类型、来源资产类型、来源资产 ID,为企业微信资产增加手机号关联方式。
2. 将历史手机号资产初始化为 `SELF`,并为新字段建立筛选索引。 2. 将历史手机号码管理初始化为 `SELF`,并为新字段建立筛选索引。
3. 发布后先验证现有列表仍可正常查询,再验证新建企微资产的已有号码和新建号码两条流程。 3. 发布后先验证现有列表仍可正常查询,再验证新建企微资产的已有号码和新建号码两条流程。
4. 回滚时保留新列但停止写入;前后端对空值回退为“自有号码”,避免旧数据不可读。 4. 回滚时保留新列但停止写入;前后端对空值回退为“自有号码”,避免旧数据不可读。
......
## Why ## Why
企业微信资料页目前只有只读列表,且布局与手机号资产页不一致。登记企业微信账号时无法复用或安全地新增注册手机号,也无法区分手机号是自有号码还是由外部资产登记流程创建的号码。 企业微信资料页目前只有只读列表,且布局与手机号码管理页不一致。登记企业微信账号时无法复用或安全地新增注册手机号,也无法区分手机号是自有号码还是由外部资产登记流程创建的号码。
## What Changes ## What Changes
- 将“企微资料”页面改为“企业微信资产”,复用手机号资产页的标题、按钮位置、面板宽度和表格内部横向滚动体验。 - 将“企微资料”页面改为“企业微信资产”,复用手机号码管理页的标题、按钮位置、面板宽度和表格内部横向滚动体验。
- 新增企业微信资产创建表单;注册手机号必填,支持搜索已有手机号资产,或在号码不存在时自动创建手机号资产 - 新增企业微信资产创建表单;注册手机号必填,支持搜索已有手机号码管理,或在号码不存在时自动创建手机号码管理
- 新增手机号号码类型:直接在手机号资产页创建的号码为“自有号码”,由非手机号资产创建流程自动创建的号码为“外部号码”。 - 新增手机号号码类型:直接在手机号码管理页创建的号码为“自有号码”,由非手机号码管理创建流程自动创建的号码为“外部号码”。
- 为外部号码保存来源资产信息;手机号资产页将“外部号码”显示为无下划线的蓝色链接,点击后进入来源资产列表并按手机号筛选关联记录。 - 为外部号码保存来源资产信息;手机号码管理页将“外部号码”显示为无下划线的蓝色链接,点击后进入来源资产列表并按手机号筛选关联记录。
- 新增注册主体与企微号归属人的搜索选择能力;注册主体使用公司简称显示,企微号归属人复用公司人员记录。 - 新增注册主体与企微号归属人的搜索选择能力;注册主体使用公司简称显示,企微号归属人复用公司人员记录。
- 企业微信资产列表新增注册手机号筛选,并展示“已有号码”或“新建号码”的关联方式。 - 企业微信资产列表新增注册手机号筛选,并展示“已有号码”或“新建号码”的关联方式。
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
- `wecom-account-creation`: 创建企业微信资产,并完成注册主体、注册手机号和归属人员的选择与保存。 - `wecom-account-creation`: 创建企业微信资产,并完成注册主体、注册手机号和归属人员的选择与保存。
- `phone-asset-origin-tracking`: 保存手机号号码类型及来源资产,并支持从外部号码跳转到对应资产列表。 - `phone-asset-origin-tracking`: 保存手机号号码类型及来源资产,并支持从外部号码跳转到对应资产列表。
- `asset-reference-lookups`: 为企业微信资产表单提供注册主体、手机号资产和公司人员的搜索选择接口。 - `asset-reference-lookups`: 为企业微信资产表单提供注册主体、手机号码管理和公司人员的搜索选择接口。
### Modified Capabilities ### Modified Capabilities
...@@ -25,6 +25,6 @@ ...@@ -25,6 +25,6 @@
## Impact ## Impact
- 前端:企业微信资产页、手机号资产页、对应 API 客户端、公共样式及 Playwright 测试。 - 前端:企业微信资产页、手机号码管理页、对应 API 客户端、公共样式及 Playwright 测试。
- 后端:企业微信和手机号资产的 Controller、Service、DTO、响应对象与单元测试。 - 后端:企业微信和手机号码管理的 Controller、Service、DTO、响应对象与单元测试。
- 数据库:手机号资产表新增号码类型和来源资产字段;不在本次实现中直接执行数据库变更。 - 数据库:手机号码管理表新增号码类型和来源资产字段;不在本次实现中直接执行数据库变更。
## ADDED Requirements ## ADDED Requirements
### Requirement: 资产引用搜索 ### Requirement: 资产引用搜索
系统 SHALL 提供公司档案、手机号资产和公司人员的只读搜索接口,供企业微信资产表单选择引用资产。 系统 SHALL 提供公司档案、手机号码管理和公司人员的只读搜索接口,供企业微信资产表单选择引用资产。
#### Scenario: 搜索注册主体 #### Scenario: 搜索注册主体
- **WHEN** 用户输入公司名称或简称 - **WHEN** 用户输入公司名称或简称
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
#### Scenario: 搜索注册手机号 #### Scenario: 搜索注册手机号
- **WHEN** 用户输入手机号片段 - **WHEN** 用户输入手机号片段
- **THEN** 系统 MUST 返回有效手机号资产的 ID、手机号与号码类型,且不返回已删除资产 - **THEN** 系统 MUST 返回有效手机号码管理的 ID、手机号与号码类型,且不返回已删除资产
## ADDED Requirements ## ADDED Requirements
### Requirement: 手机号号码类型 ### Requirement: 手机号号码类型
系统 SHALL 为手机号资产保存并显示号码类型;手机号资产页直接创建的号码为“自有号码”,由非手机号资产创建流程自动创建的号码为“外部号码”。 系统 SHALL 为手机号码管理保存并显示号码类型;手机号码管理页直接创建的号码为“自有号码”,由非手机号码管理创建流程自动创建的号码为“外部号码”。
#### Scenario: 显示外部号码 #### Scenario: 显示外部号码
- **WHEN** 手机号资产的号码类型为外部号码 - **WHEN** 手机号码管理的号码类型为外部号码
- **THEN** 手机号资产列表 MUST 以无下划线的蓝色文字显示“外部号码” - **THEN** 手机号码管理列表 MUST 以无下划线的蓝色文字显示“外部号码”
### Requirement: 外部号码来源跳转 ### Requirement: 外部号码来源跳转
系统 SHALL 为外部号码保存来源资产,并允许用户点击号码类型跳转到来源资产列表且按该手机号筛选。 系统 SHALL 为外部号码保存来源资产,并允许用户点击号码类型跳转到来源资产列表且按该手机号筛选。
#### Scenario: 跳转企业微信资产 #### Scenario: 跳转企业微信资产
- **WHEN** 用户点击来源为企业微信资产的外部号码 - **WHEN** 用户点击来源为企业微信资产的外部号码
- **THEN** 系统 MUST 打开企业微信资产列表并仅显示关联该手机号资产的记录 - **THEN** 系统 MUST 打开企业微信资产列表并仅显示关联该手机号码管理的记录
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
系统 SHALL 在企业微信资产页提供“新增企业微信资产”按钮及表单;企微名称和注册手机号 MUST 必填,企微别名 MUST 默认填入“记忆力梅老师-助教老师”。 系统 SHALL 在企业微信资产页提供“新增企业微信资产”按钮及表单;企微名称和注册手机号 MUST 必填,企微别名 MUST 默认填入“记忆力梅老师-助教老师”。
#### Scenario: 以已有号码创建 #### Scenario: 以已有号码创建
- **WHEN** 用户选择一个有效的已有手机号资产并保存企业微信资产 - **WHEN** 用户选择一个有效的已有手机号码管理并保存企业微信资产
- **THEN** 系统 MUST 保存该手机号资产 ID,并在企业微信资产列表显示“已有号码” - **THEN** 系统 MUST 保存该手机号码管理 ID,并在企业微信资产列表显示“已有号码”
#### Scenario: 以新号码创建 #### Scenario: 以新号码创建
- **WHEN** 用户输入一个不存在的有效手机号并保存企业微信资产 - **WHEN** 用户输入一个不存在的有效手机号并保存企业微信资产
- **THEN** 系统 MUST 原子地创建外部号码手机号资产与企业微信资产,并在列表显示“新建号码” - **THEN** 系统 MUST 原子地创建外部号码手机号码管理与企业微信资产,并在列表显示“新建号码”
### Requirement: 企业微信资产表单字段 ### Requirement: 企业微信资产表单字段
系统 SHALL 提供注册主体搜索、注册手机号搜索、实名状态单选、性别单选与企微号归属人搜索;注册主体和归属人不是必填项,设备 ID 不在表单中出现。 系统 SHALL 提供注册主体搜索、注册手机号搜索、实名状态单选、性别单选与企微号归属人搜索;注册主体和归属人不是必填项,设备 ID 不在表单中出现。
......
## 1. 数据模型与后端接口 ## 1. 数据模型与后端接口
- [x] 1.1 为手机号资产和企业微信资产补充号码来源、关联方式及对应 DTO/响应字段。 - [x] 1.1 为手机号码管理和企业微信资产补充号码来源、关联方式及对应 DTO/响应字段。
- [x] 1.2 实现注册主体、手机号资产和公司人员的只读搜索接口。 - [x] 1.2 实现注册主体、手机号码管理和公司人员的只读搜索接口。
- [x] 1.3 实现企业微信资产创建接口,并在新手机号场景中原子创建手机号资产 - [x] 1.3 实现企业微信资产创建接口,并在新手机号场景中原子创建手机号码管理
- [x] 1.4 实现按手机号资产筛选企业微信资产的查询兼容逻辑。 - [x] 1.4 实现按手机号码管理筛选企业微信资产的查询兼容逻辑。
## 2. 前端页面与交互 ## 2. 前端页面与交互
- [x] 2.1 将企业微信资产页改为手机号资产页一致的标题、按钮、面板和宽表布局。 - [x] 2.1 将企业微信资产页改为手机号码管理页一致的标题、按钮、面板和宽表布局。
- [x] 2.2 实现企业微信资产新增弹窗及注册主体、注册手机号、归属人员的搜索选择。 - [x] 2.2 实现企业微信资产新增弹窗及注册主体、注册手机号、归属人员的搜索选择。
- [x] 2.3 在手机号资产列表显示号码类型,并实现外部号码蓝色无下划线跳转。 - [x] 2.3 在手机号码管理列表显示号码类型,并实现外部号码蓝色无下划线跳转。
- [x] 2.4 实现企业微信资产页接收手机号筛选参数并显示关联记录。 - [x] 2.4 实现企业微信资产页接收手机号筛选参数并显示关联记录。
## 3. 数据库交付与验证 ## 3. 数据库交付与验证
......
## Context ## Context
当前 `#/reference/wechat` 是不请求接口的静态旧界面参考,而 `as_wecom_account` 已经具备 `WecomAccountEntity``WecomAccountMapper`,尚未具备列表 Service、Controller、DTO 与真实前端页面。资产人员需要查看企微资料的全部业务字段,并将公司档案、手机号资产、设备、经办人等关联 ID 理解为可读名称。 当前 `#/reference/wechat` 是不请求接口的静态旧界面参考,而 `as_wecom_account` 已经具备 `WecomAccountEntity``WecomAccountMapper`,尚未具备列表 Service、Controller、DTO 与真实前端页面。资产人员需要查看企微资料的全部业务字段,并将公司档案、手机号码管理、设备、经办人等关联 ID 理解为可读名称。
约束:本次只读主数据源,不修改表结构、不新增编辑删除能力;`delete_time` 延续现有手机号资产规则,仅用于过滤;用户已确认它不应返回或展示。 约束:本次只读主数据源,不修改表结构、不新增编辑删除能力;`delete_time` 延续现有手机号码管理规则,仅用于过滤;用户已确认它不应返回或展示。
## Goals / Non-Goals ## Goals / Non-Goals
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
### 分页与筛选契约 ### 分页与筛选契约
接口使用现有手机号资产`page``size` 约定:默认第 1 页、每页 20 条、最大 100 条;支持精确筛选 `wecomName``wecomAccount`。查询显式限定 `delete_time = 0` 并按 `id` 倒序,保证与既有资产列表的正常记录定义一致。 接口使用现有手机号码管理`page``size` 约定:默认第 1 页、每页 20 条、最大 100 条;支持精确筛选 `wecomName``wecomAccount`。查询显式限定 `delete_time = 0` 并按 `id` 倒序,保证与既有资产列表的正常记录定义一致。
### 关联 ID 与名称 ### 关联 ID 与名称
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
### 字段暴露与页面展示 ### 字段暴露与页面展示
响应和表格展示 `id`、企微名称、别名、账号、公司档案 ID/名称、手机号资产 ID/手机号、企微实名人、实名状态、性别、设备 ID/名称、经办人 ID/名称、创建时间和更新时间。`deleteTime` 只在 Service 查询条件中使用,既不写入响应 DTO,也不创建表格列。 响应和表格展示 `id`、企微名称、别名、账号、公司档案 ID/名称、手机号码管理 ID/手机号、企微实名人、实名状态、性别、设备 ID/名称、经办人 ID/名称、创建时间和更新时间。`deleteTime` 只在 Service 查询条件中使用,既不写入响应 DTO,也不创建表格列。
## Risks / Trade-offs ## Risks / Trade-offs
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
系统 MUST 在每条列表记录中返回 `id``wecomName``wecomAlias``wecomAccount``companyProfileId``phoneAssetId``realNameOwner``realNameOwnerStatus``gender``deviceId``operatorPersonId``createTime``updateTime`,并同时返回关联名称字段。 系统 MUST 在每条列表记录中返回 `id``wecomName``wecomAlias``wecomAccount``companyProfileId``phoneAssetId``realNameOwner``realNameOwnerStatus``gender``deviceId``operatorPersonId``createTime``updateTime`,并同时返回关联名称字段。
#### Scenario: 关联 ID 可读化 #### Scenario: 关联 ID 可读化
- **WHEN** 企微资产的公司档案、手机号资产、设备或经办人 ID 可以关联到正常的资产记录 - **WHEN** 企微资产的公司档案、手机号码管理、设备或经办人 ID 可以关联到正常的资产记录
- **THEN** 响应分别包含 `companyProfileName``phoneNumber``deviceName``operatorPersonName`,同时保留原始 ID - **THEN** 响应分别包含 `companyProfileName``phoneNumber``deviceName``operatorPersonName`,同时保留原始 ID
#### Scenario: 关联记录缺失 #### Scenario: 关联记录缺失
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
系统 MUST 在企微资料列表中展示企微资产 ID、企微名称、别名、账号、企微实名人、实名状态、性别、创建时间、更新时间,以及每个关联资源的“名称(ID)”。 系统 MUST 在企微资料列表中展示企微资产 ID、企微名称、别名、账号、企微实名人、实名状态、性别、创建时间、更新时间,以及每个关联资源的“名称(ID)”。
#### Scenario: 关联名称与 ID 同时展示 #### Scenario: 关联名称与 ID 同时展示
- **WHEN** 列表记录包含公司档案、手机号资产、设备或经办人关联 - **WHEN** 列表记录包含公司档案、手机号码管理、设备或经办人关联
- **THEN** 页面分别展示公司名称、手机号、设备名称、人员名称及对应 ID - **THEN** 页面分别展示公司名称、手机号、设备名称、人员名称及对应 ID
#### Scenario: 关联名称不可用 #### Scenario: 关联名称不可用
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- [x] 1.1 新增 `WecomAccountPageQuery.java`(文件用途:接收分页与筛选参数),为 `resolvedPage``resolvedSize` 写新手注释:代码作用(白话)、关联文件、关联逻辑;校验页码与页大小。 - [x] 1.1 新增 `WecomAccountPageQuery.java`(文件用途:接收分页与筛选参数),为 `resolvedPage``resolvedSize` 写新手注释:代码作用(白话)、关联文件、关联逻辑;校验页码与页大小。
- [x] 1.2 新增 `WecomAccountResponse.java`(文件用途:限定企微列表单行返回字段),包含企微业务字段、创建/更新时间、关联 ID 与关联名称,不包含 `deleteTime` - [x] 1.2 新增 `WecomAccountResponse.java`(文件用途:限定企微列表单行返回字段),包含企微业务字段、创建/更新时间、关联 ID 与关联名称,不包含 `deleteTime`
- [x] 1.3 新增 `WecomAccountPageResponse.java`(文件用途:承载列表、总数、页码、页大小),复用当前手机号资产分页的返回结构。 - [x] 1.3 新增 `WecomAccountPageResponse.java`(文件用途:承载列表、总数、页码、页大小),复用当前手机号码管理分页的返回结构。
## 2. 后端只读列表实现 ## 2. 后端只读列表实现
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
### Requirement: 一期字段与索引契约 ### Requirement: 一期字段与索引契约
系统 SHALL 按设计文档的 11 表字段字典创建业务列、主键、复合唯一索引和查询索引。逻辑关联字段 MUST 保存为 `*_id``BIGINT UNSIGNED` 列,但数据库 MUST NOT 定义 `FOREIGN KEY``REFERENCES``CHECK` 系统 SHALL 按设计文档的 11 表字段字典创建业务列、主键、复合唯一索引和查询索引。逻辑关联字段 MUST 保存为 `*_id``BIGINT UNSIGNED` 列,但数据库 MUST NOT 定义 `FOREIGN KEY``REFERENCES``CHECK`
#### Scenario: 验证手机号资产关联快照 #### Scenario: 验证手机号码管理关联快照
- **WHEN** 验收 `phone_asset` 的结构 - **WHEN** 验收 `phone_asset` 的结构
- **THEN** 必须存在手机号、卡/实名/设备字段、五个 `JSON` 关联快照字段、`relation_synced_at`、手机号有效记录复合唯一索引,以及卡类型、ICCID、设备查询索引 - **THEN** 必须存在手机号、卡/实名/设备字段、五个 `JSON` 关联快照字段、`relation_synced_at`、手机号有效记录复合唯一索引,以及卡类型、ICCID、设备查询索引
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
#### Scenario: 保留本次业务修复 #### Scenario: 保留本次业务修复
- **WHEN** 完成动画工作内容清理后检查认证和手机号文件 - **WHEN** 完成动画工作内容清理后检查认证和手机号文件
- **THEN** 登录会话诊断、接口真实错误提示、企微注册手机号和手机号资产输入限制改动仍然存在 - **THEN** 登录会话诊断、接口真实错误提示、企微注册手机号和手机号码管理输入限制改动仍然存在
...@@ -38,7 +38,7 @@ DTO 处理格式完整的请求校验,服务层处理去空格后的长度与 ...@@ -38,7 +38,7 @@ DTO 处理格式完整的请求校验,服务层处理去空格后的长度与
不以“先查再写”作为重复保护。数据库现有唯一索引在并发保存时仍可阻止重复,GlobalExceptionHandler 根据公司名称唯一索引返回 HTTP 409 和“公司名称已存在”。 不以“先查再写”作为重复保护。数据库现有唯一索引在并发保存时仍可阻止重复,GlobalExceptionHandler 根据公司名称唯一索引返回 HTTP 409 和“公司名称已存在”。
### 前端使用现有手机号资产计数外观并安全解析响应 ### 前端使用现有手机号码管理计数外观并安全解析响应
公司名称输入框复用 `phone-asset-modal__count-input``phone-asset-modal__character-count`。页面显示字符数、阻止 emoji 保存,并在非 JSON 或空响应时显示固定安全提示。 公司名称输入框复用 `phone-asset-modal__count-input``phone-asset-modal__character-count`。页面显示字符数、阻止 emoji 保存,并在非 JSON 或空响应时显示固定安全提示。
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
### Requirement: 公司档案新增弹窗 ### Requirement: 公司档案新增弹窗
系统 SHALL 仅向具有 `company-profile` EDIT 权限的用户显示“新增公司档案”按钮。弹窗 MUST 复用手机号资产新增弹窗的布局和操作样式,标记公司名称为必填,其余五个业务字段为可选;页面不得展示内部 ID。 系统 SHALL 仅向具有 `company-profile` EDIT 权限的用户显示“新增公司档案”按钮。弹窗 MUST 复用手机号码管理新增弹窗的布局和操作样式,标记公司名称为必填,其余五个业务字段为可选;页面不得展示内部 ID。
公司名称输入框 MUST 显示当前字符数与上限 30 的计数,且不得允许 emoji 进入可保存值。统一社会信用代码和联系人输入框 MUST 限制为 64 个字符。保存失败时,弹窗 MUST 保持打开并显示服务端返回的可读错误;保存成功后 MUST 关闭弹窗并刷新公司档案列表。 公司名称输入框 MUST 显示当前字符数与上限 30 的计数,且不得允许 emoji 进入可保存值。统一社会信用代码和联系人输入框 MUST 限制为 64 个字符。保存失败时,弹窗 MUST 保持打开并显示服务端返回的可读错误;保存成功后 MUST 关闭弹窗并刷新公司档案列表。
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
## 3. 前端交互与响应健壮性 ## 3. 前端交互与响应健壮性
- [ ] 3.1 更新 CompanyProfileView:复用手机号资产字符计数样式,为公司名称显示 `当前值/30`,阻止 emoji 保存,并将统一社会信用代码和联系人限制为 64;为新增或改动函数补齐新手注释。 - [ ] 3.1 更新 CompanyProfileView:复用手机号码管理字符计数样式,为公司名称显示 `当前值/30`,阻止 emoji 保存,并将统一社会信用代码和联系人限制为 64;为新增或改动函数补齐新手注释。
- [ ] 3.2 更新 company-profile-api-client:安全处理空响应、HTML 和无法解析的响应,向页面返回“服务响应异常,请稍后重试”,并保留已有成功和业务错误处理。 - [ ] 3.2 更新 company-profile-api-client:安全处理空响应、HTML 和无法解析的响应,向页面返回“服务响应异常,请稍后重试”,并保留已有成功和业务错误处理。
## 4. 本地错误日志 ## 4. 本地错误日志
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment