Commit 90002afb by DaiJiezhang

Merge branch 'feat/device-image-and-wecom-crud' into 'master'

feat: add company people management

See merge request !14
parents 9ae57966 36b73908
package com.xyw.console.asset.controller;
import com.xyw.console.common.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** 文件用途(白话):把公司档案和公司人员的字段、引用与不存在错误转换成页面可读 JSON。 */
@RestControllerAdvice(assignableTypes={CompanyProfileController.class,CompanyPersonController.class})
public class CompanyManagementExceptionHandler {
/** 代码作用(白话):把保存校验、记录不存在和引用保护提示返回为 400。关联文件:CompanyProfileService.java、CompanyPersonService.java。关联逻辑(调用链/数据流):Service 参数错误 -> Advice -> ApiResponse -> 页面提示。 */
@ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> invalid(IllegalArgumentException error){return ApiResponse.error(400,error.getMessage());}
}
package com.xyw.console.asset.controller;
import com.xyw.console.asset.dto.CompanyPersonPageQuery;
import com.xyw.console.asset.dto.CompanyPersonPageResponse;
import com.xyw.console.asset.dto.CompanyPersonResponse;
import com.xyw.console.asset.dto.CompanyPersonSaveRequest;
import com.xyw.console.asset.dto.CompanyProfileLookupResponse;
import com.xyw.console.asset.service.CompanyPersonService;
import com.xyw.console.auth.PagePermissionService;
import com.xyw.console.common.ApiResponse;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 文件用途(白话):提供公司人员分页、公司搜索和增删改接口,并统一检查独立页面权限。 */
@RestController
@RequestMapping("/api/company-persons")
public class CompanyPersonController {
private final CompanyPersonService service; private final PagePermissionService permissions;
/** 代码作用(白话):接收人员服务和权限服务。关联文件:CompanyPersonService.java、PagePermissionService.java。关联逻辑(调用链/数据流):HTTP -> Controller -> 权限/Service。 */
public CompanyPersonController(CompanyPersonService service,PagePermissionService permissions){this.service=service;this.permissions=permissions;}
/** 代码作用(白话):读取有权限用户可见的公司人员分页。关联文件:CompanyPersonPageQuery.java、CompanyPersonView.js。关联逻辑(调用链/数据流):GET -> READ -> Service.page -> JSON。 */
@GetMapping public ApiResponse<CompanyPersonPageResponse> page(@Valid CompanyPersonPageQuery query){permissions.require(PagePermissionService.COMPANY_PERSON,"READ");return ApiResponse.success(service.page(query));}
/** 代码作用(白话):为人员表单搜索有效所属公司。关联文件:CompanyPersonService.java、CompanyPersonView.js。关联逻辑(调用链/数据流):关键词 -> READ -> 公司选项。 */
@GetMapping("/lookups/company-profiles") public ApiResponse<List<CompanyProfileLookupResponse>> companies(@RequestParam(defaultValue="")String keyword){permissions.require(PagePermissionService.COMPANY_PERSON,"READ");return ApiResponse.success(service.searchCompanies(keyword));}
/** 代码作用(白话):新增公司人员。关联文件:CompanyPersonSaveRequest.java、CompanyPersonService.java。关联逻辑(调用链/数据流):POST 表单 -> EDIT -> create -> JSON。 */
@PostMapping public ApiResponse<CompanyPersonResponse> create(@Valid @RequestBody CompanyPersonSaveRequest request){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");return ApiResponse.success("新增成功",service.create(request));}
/** 代码作用(白话):编辑指定公司人员。关联文件:CompanyPersonSaveRequest.java、CompanyPersonService.java。关联逻辑(调用链/数据流):PUT /{id} -> EDIT -> update -> JSON。 */
@PutMapping("/{id}") public ApiResponse<CompanyPersonResponse> update(@PathVariable Long id,@Valid @RequestBody CompanyPersonSaveRequest request){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");return ApiResponse.success("修改成功",service.update(id,request));}
/** 代码作用(白话):引用检查通过后软删除公司人员。关联文件:CompanyPersonService.java、CompanyPersonView.js。关联逻辑(调用链/数据流):DELETE /{id} -> EDIT -> softDelete -> JSON。 */
@DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);}
}
...@@ -10,6 +10,9 @@ import com.xyw.console.common.ApiResponse; ...@@ -10,6 +10,9 @@ import com.xyw.console.common.ApiResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
...@@ -52,4 +55,19 @@ public class CompanyProfileController { ...@@ -52,4 +55,19 @@ public class CompanyProfileController {
permissions.require(PagePermissionService.COMPANY_PROFILE, "EDIT"); permissions.require(PagePermissionService.COMPANY_PROFILE, "EDIT");
return ApiResponse.success("新增成功", service.create(request)); return ApiResponse.success("新增成功", service.create(request));
} }
/** 代码作用(白话):校验编辑权限后更新指定公司档案。关联文件:CompanyProfileService.java、CompanyProfileView.js。关联逻辑(调用链/数据流):PUT /{id} -> EDIT 权限 -> Service.update -> JSON。 */
@PutMapping("/{id}")
public ApiResponse<CompanyProfileResponse> update(@PathVariable Long id, @Valid @RequestBody CompanyProfileSaveRequest request) {
permissions.require(PagePermissionService.COMPANY_PROFILE, "EDIT");
return ApiResponse.success("修改成功", service.update(id, request));
}
/** 代码作用(白话):校验编辑权限后发起引用安全软删除。关联文件:CompanyProfileService.java、CompanyProfileView.js。关联逻辑(调用链/数据流):DELETE /{id} -> EDIT 权限 -> 引用检查/软删除 -> JSON。 */
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
permissions.require(PagePermissionService.COMPANY_PROFILE, "EDIT");
service.softDelete(id);
return ApiResponse.success("删除成功", null);
}
} }
package com.xyw.console.asset.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
/** 文件用途(白话):接收公司人员列表的分页、姓名、所属公司和状态筛选条件。 */
public record CompanyPersonPageQuery(
@Min(1) Integer page,
@Min(1) @Max(100) Integer size,
String keyword,
Long companyProfileId,
String employmentStatus) {
/** 代码作用(白话):未传页码时使用第一页。关联文件:CompanyPersonService.java。关联逻辑(调用链/数据流):HTTP 参数 -> 默认页码 -> 数据库分页。 */
public int resolvedPage() { return page == null ? 1 : page; }
/** 代码作用(白话):未传每页数量时使用 20 条。关联文件:CompanyPersonService.java、CompanyPersonView.js。关联逻辑(调用链/数据流):HTTP 参数 -> 默认数量 -> 数据库分页。 */
public int resolvedSize() { return size == null ? 20 : size; }
}
package com.xyw.console.asset.dto;
import java.util.List;
/** 文件用途(白话):把公司人员记录、总数和分页位置一起返回给列表页面。 */
public record CompanyPersonPageResponse(List<CompanyPersonResponse> records, long total, int page, int size) {
}
package com.xyw.console.asset.dto;
import java.time.LocalDateTime;
/** 文件用途(白话):定义公司人员页面可读取的字段,并补充所属公司的可读名称。 */
public record CompanyPersonResponse(
Long id,
Long companyProfileId,
String companyProfileName,
String personName,
String employmentStatus,
LocalDateTime resignedAt,
LocalDateTime createTime,
LocalDateTime updateTime) {
}
package com.xyw.console.asset.dto;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
/** 文件用途(白话):定义公司人员新增和编辑时允许浏览器提交的业务字段,不接受主键或审计字段。 */
public record CompanyPersonSaveRequest(
Long companyProfileId,
@NotBlank(message = "人员姓名不能为空") String personName,
String employmentStatus,
LocalDateTime resignedAt) {
}
...@@ -4,6 +4,7 @@ import java.time.LocalDateTime; ...@@ -4,6 +4,7 @@ import java.time.LocalDateTime;
/** 文件用途(白话):定义公司档案页面允许展示的字段,不暴露内部 ID 和软删除标志。 */ /** 文件用途(白话):定义公司档案页面允许展示的字段,不暴露内部 ID 和软删除标志。 */
public record CompanyProfileResponse( public record CompanyProfileResponse(
Long id,
String companyName, String companyName,
String shortName, String shortName,
String unifiedSocialCreditCode, String unifiedSocialCreditCode,
......
package com.xyw.console.asset.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xyw.console.asset.dto.CompanyPersonPageQuery;
import com.xyw.console.asset.dto.CompanyPersonPageResponse;
import com.xyw.console.asset.dto.CompanyPersonResponse;
import com.xyw.console.asset.dto.CompanyPersonSaveRequest;
import com.xyw.console.asset.dto.CompanyProfileLookupResponse;
import com.xyw.console.asset.entity.AssetDeviceEntity;
import com.xyw.console.asset.entity.CompanyPersonEntity;
import com.xyw.console.asset.entity.CompanyProfileEntity;
import com.xyw.console.asset.entity.DouyinAccountEntity;
import com.xyw.console.asset.entity.WechatAccountEntity;
import com.xyw.console.asset.entity.WecomAccountEntity;
import com.xyw.console.asset.mapper.AssetDeviceMapper;
import com.xyw.console.asset.mapper.CompanyPersonMapper;
import com.xyw.console.asset.mapper.CompanyProfileMapper;
import com.xyw.console.asset.mapper.DouyinAccountMapper;
import com.xyw.console.asset.mapper.WechatAccountMapper;
import com.xyw.console.asset.mapper.WecomAccountMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
/** 文件用途(白话):负责公司人员分页、保存、公司名称补全和引用安全软删除。 */
@Service
public class CompanyPersonService {
private final CompanyPersonMapper mapper; private final CompanyProfileMapper companyMapper; private final AssetDeviceMapper deviceMapper;
private final WecomAccountMapper wecomMapper; private final WechatAccountMapper wechatMapper; private final DouyinAccountMapper douyinMapper;
/** 代码作用(白话):接收人员、公司及引用资产的数据入口。关联文件:CompanyPersonController.java、各 Mapper。关联逻辑(调用链/数据流):Controller -> Service -> Mapper。 */
public CompanyPersonService(CompanyPersonMapper mapper, CompanyProfileMapper companyMapper, AssetDeviceMapper deviceMapper,
WecomAccountMapper wecomMapper, WechatAccountMapper wechatMapper, DouyinAccountMapper douyinMapper) {
this.mapper=mapper; this.companyMapper=companyMapper; this.deviceMapper=deviceMapper; this.wecomMapper=wecomMapper; this.wechatMapper=wechatMapper; this.douyinMapper=douyinMapper;
}
/** 代码作用(白话):按人员姓名、公司和状态分页并补齐公司显示名。关联文件:CompanyPersonPageQuery.java、CompanyPersonView.js。关联逻辑(调用链/数据流):GET 参数 -> 人员分页 -> 公司批量查询 -> 页面记录。 */
public CompanyPersonPageResponse page(CompanyPersonPageQuery query) {
Page<CompanyPersonEntity> page=mapper.selectPage(new Page<>(query.resolvedPage(),query.resolvedSize()), activeQuery(query));
Map<Long,String> names=companyNames(page.getRecords().stream().map(CompanyPersonEntity::getCompanyProfileId).filter(java.util.Objects::nonNull).collect(Collectors.toSet()));
return new CompanyPersonPageResponse(page.getRecords().stream().map(item->toResponse(item,item.getCompanyProfileId()==null?null:names.get(item.getCompanyProfileId()))).toList(),page.getTotal(),query.resolvedPage(),query.resolvedSize());
}
/** 代码作用(白话):新增公司人员并初始化默认状态与审计字段。关联文件:CompanyPersonSaveRequest.java、CompanyPersonMapper.java。关联逻辑(调用链/数据流):POST -> 校验/复制字段 -> insert -> 页面响应。 */
public CompanyPersonResponse create(CompanyPersonSaveRequest request) {
CompanyProfileEntity company=validateAndGetCompany(request.companyProfileId()); CompanyPersonEntity entity=new CompanyPersonEntity(); applyEditableFields(entity,request); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); if(mapper.insert(entity)!=1) throw new IllegalStateException("公司人员新增失败"); return toResponse(entity,displayName(company));
}
/** 代码作用(白话):编辑一条有效人员并保持创建时间不变。关联文件:CompanyPersonController.java、CompanyPersonView.js。关联逻辑(调用链/数据流):PUT -> 有效人员/公司校验 -> updateById -> 页面响应。 */
public CompanyPersonResponse update(Long id, CompanyPersonSaveRequest request) {
CompanyPersonEntity entity=requireActive(id); CompanyProfileEntity company=validateAndGetCompany(request.companyProfileId()); applyEditableFields(entity,request); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new IllegalArgumentException("公司人员不存在或已删除"); return toResponse(entity,displayName(company));
}
/** 代码作用(白话):没有有效资产引用时软删除人员。关联文件:设备、企微、微信、抖音实体。关联逻辑(调用链/数据流):DELETE -> 引用计数 -> deleteTime -> updateById。 */
public void softDelete(Long id) { CompanyPersonEntity entity=requireActive(id); checkActiveReferences(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new IllegalArgumentException("公司人员不存在或已删除"); }
/** 代码作用(白话):按公司名称或简称搜索可选所属公司。关联文件:CompanyPersonView.js、CompanyProfileLookupResponse.java。关联逻辑(调用链/数据流):远程输入 -> 公司有效记录 -> 下拉选项。 */
public List<CompanyProfileLookupResponse> searchCompanies(String keyword) { LambdaQueryWrapper<CompanyProfileEntity> query=new LambdaQueryWrapper<CompanyProfileEntity>().eq(CompanyProfileEntity::getDeleteTime,0L); if(hasText(keyword)) query.and(w->w.like(CompanyProfileEntity::getCompanyName,keyword).or().like(CompanyProfileEntity::getShortName,keyword)); return companyMapper.selectList(query.orderByDesc(CompanyProfileEntity::getId)).stream().limit(20).map(item->new CompanyProfileLookupResponse(item.getId(),item.getCompanyName(),item.getShortName())).toList(); }
/** 代码作用(白话):组合人员列表的有效记录及三个可选筛选条件。关联文件:CompanyPersonPageQuery.java。关联逻辑(调用链/数据流):筛选表单 -> SQL 条件 -> 分页记录。 */
private LambdaQueryWrapper<CompanyPersonEntity> activeQuery(CompanyPersonPageQuery query) { return new LambdaQueryWrapper<CompanyPersonEntity>().eq(CompanyPersonEntity::getDeleteTime,0L).like(hasText(query.keyword()),CompanyPersonEntity::getPersonName,query.keyword()).eq(query.companyProfileId()!=null,CompanyPersonEntity::getCompanyProfileId,query.companyProfileId()).eq(hasText(query.employmentStatus()),CompanyPersonEntity::getEmploymentStatus,query.employmentStatus()).orderByDesc(CompanyPersonEntity::getId); }
/** 代码作用(白话):校验姓名、状态、离职时间并复制到实体。关联文件:CompanyPersonSaveRequest.java、CompanyPersonEntity.java。关联逻辑(调用链/数据流):新增/编辑表单 -> 规则校验 -> 数据库字段。 */
private void applyEditableFields(CompanyPersonEntity entity,CompanyPersonSaveRequest request) { String name=request.personName()==null?"":request.personName().trim(); if(name.isEmpty()) throw new IllegalArgumentException("人员姓名不能为空"); String status=hasText(request.employmentStatus())?request.employmentStatus():"在职"; if(!Set.of("在职","离职").contains(status)) throw new IllegalArgumentException("在职状态取值无效"); if("离职".equals(status)&&request.resignedAt()==null) throw new IllegalArgumentException("离职人员必须填写离职时间"); entity.setCompanyProfileId(request.companyProfileId()); entity.setPersonName(name); entity.setEmploymentStatus(status); entity.setResignedAt("离职".equals(status)?request.resignedAt():null); }
/** 代码作用(白话):选择了公司时确认它存在且未删除,未选择时允许为空。关联文件:CompanyProfileMapper.java。关联逻辑(调用链/数据流):companyProfileId -> 有效公司或空 -> 保存人员。 */
private CompanyProfileEntity validateAndGetCompany(Long id) { if(id==null)return null; CompanyProfileEntity company=companyMapper.selectOne(new LambdaQueryWrapper<CompanyProfileEntity>().eq(CompanyProfileEntity::getId,id).eq(CompanyProfileEntity::getDeleteTime,0L)); if(company==null)throw new IllegalArgumentException("所属公司不存在或已删除"); return company; }
/** 代码作用(白话):查找一条有效人员供编辑和删除。关联文件:CompanyPersonMapper.java。关联逻辑(调用链/数据流):路径 ID -> 有效人员 -> 实体或参数错误。 */
private CompanyPersonEntity requireActive(Long id) { CompanyPersonEntity entity=mapper.selectOne(new LambdaQueryWrapper<CompanyPersonEntity>().eq(CompanyPersonEntity::getId,id).eq(CompanyPersonEntity::getDeleteTime,0L)); if(entity==null)throw new IllegalArgumentException("公司人员不存在或已删除"); return entity; }
/** 代码作用(白话):检查四类有效资产是否仍引用人员。关联文件:设备、企微、微信、抖音实体。关联逻辑(调用链/数据流):人员 ID -> 引用计数 -> 拒绝或允许删除。 */
private void checkActiveReferences(Long id) { List<String> types=new ArrayList<>(); if(deviceMapper.selectCount(new LambdaQueryWrapper<AssetDeviceEntity>().eq(AssetDeviceEntity::getUserPersonId,id).eq(AssetDeviceEntity::getDeleteTime,0L))>0)types.add("设备资产管理"); if(wecomMapper.selectCount(new LambdaQueryWrapper<WecomAccountEntity>().eq(WecomAccountEntity::getOperatorPersonId,id).eq(WecomAccountEntity::getDeleteTime,0L))>0)types.add("企业微信资产"); if(wechatMapper.selectCount(new LambdaQueryWrapper<WechatAccountEntity>().eq(WechatAccountEntity::getOperatorPersonId,id).eq(WechatAccountEntity::getDeleteTime,0L))>0)types.add("微信资产"); if(douyinMapper.selectCount(new LambdaQueryWrapper<DouyinAccountEntity>().eq(DouyinAccountEntity::getOperatorPersonId,id).eq(DouyinAccountEntity::getDeleteTime,0L))>0)types.add("抖音资产"); if(!types.isEmpty())throw new IllegalArgumentException("公司人员仍被"+String.join("、",types)+"引用,不能删除"); }
/** 代码作用(白话):批量把公司 ID 解析为简称优先的显示名。关联文件:CompanyProfileEntity.java。关联逻辑(调用链/数据流):人员页公司 ID 集合 -> 单次公司查询 -> 名称 Map。 */
private Map<Long,String> companyNames(Set<Long> ids) { if(ids.isEmpty())return Map.of(); Map<Long,String> result=new HashMap<>(); companyMapper.selectList(new LambdaQueryWrapper<CompanyProfileEntity>().in(CompanyProfileEntity::getId,ids).eq(CompanyProfileEntity::getDeleteTime,0L)).forEach(item->result.put(item.getId(),displayName(item))); return result; }
/** 代码作用(白话):优先使用公司简称,没有简称时显示全称。关联文件:CompanyPersonView.js。关联逻辑(调用链/数据流):公司实体 -> 可读标签 -> 列表/下拉。 */
private String displayName(CompanyProfileEntity company) { return company==null?null:(hasText(company.getShortName())?company.getShortName():company.getCompanyName()); }
/** 代码作用(白话):把人员实体和公司名称组合成安全响应。关联文件:CompanyPersonResponse.java。关联逻辑(调用链/数据流):数据库实体 -> DTO -> JSON -> 表格。 */
private CompanyPersonResponse toResponse(CompanyPersonEntity entity,String companyName) { return new CompanyPersonResponse(entity.getId(),entity.getCompanyProfileId(),companyName,entity.getPersonName(),entity.getEmploymentStatus(),entity.getResignedAt(),entity.getCreateTime(),entity.getUpdateTime()); }
/** 代码作用(白话):判断文本是否包含非空白内容。关联文件:CompanyPersonPageQuery.java。关联逻辑(调用链/数据流):请求文本 -> 是否追加筛选或使用默认值。 */
private boolean hasText(String value) { return value!=null&&!value.isBlank(); }
}
...@@ -7,7 +7,18 @@ import com.xyw.console.asset.dto.CompanyProfilePageResponse; ...@@ -7,7 +7,18 @@ import com.xyw.console.asset.dto.CompanyProfilePageResponse;
import com.xyw.console.asset.dto.CompanyProfileResponse; import com.xyw.console.asset.dto.CompanyProfileResponse;
import com.xyw.console.asset.dto.CompanyProfileSaveRequest; import com.xyw.console.asset.dto.CompanyProfileSaveRequest;
import com.xyw.console.asset.entity.CompanyProfileEntity; import com.xyw.console.asset.entity.CompanyProfileEntity;
import com.xyw.console.asset.entity.CompanyPersonEntity;
import com.xyw.console.asset.entity.DomainAssetEntity;
import com.xyw.console.asset.entity.DouyinAccountEntity;
import com.xyw.console.asset.entity.MerchantEntity;
import com.xyw.console.asset.entity.WecomAccountEntity;
import com.xyw.console.asset.mapper.CompanyPersonMapper;
import com.xyw.console.asset.mapper.CompanyProfileMapper; import com.xyw.console.asset.mapper.CompanyProfileMapper;
import com.xyw.console.asset.mapper.DomainAssetMapper;
import com.xyw.console.asset.mapper.DouyinAccountMapper;
import com.xyw.console.asset.mapper.MerchantMapper;
import com.xyw.console.asset.mapper.WecomAccountMapper;
import java.util.ArrayList;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -16,14 +27,26 @@ import org.springframework.stereotype.Service; ...@@ -16,14 +27,26 @@ import org.springframework.stereotype.Service;
@Service @Service
public class CompanyProfileService { public class CompanyProfileService {
private final CompanyProfileMapper mapper; private final CompanyProfileMapper mapper;
private final CompanyPersonMapper personMapper;
private final WecomAccountMapper wecomMapper;
private final DouyinAccountMapper douyinMapper;
private final DomainAssetMapper domainMapper;
private final MerchantMapper merchantMapper;
/** /**
* 代码作用(白话):接收公司档案表的数据库访问入口,供只读列表查询使用。 * 代码作用(白话):接收公司档案表的数据库访问入口,供只读列表查询使用。
* 关联文件:CompanyProfileMapper.java、CompanyProfileController.java。 * 关联文件:CompanyProfileMapper.java、CompanyProfileController.java。
* 关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_company_profile。 * 关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_company_profile。
*/ */
public CompanyProfileService(CompanyProfileMapper mapper) { public CompanyProfileService(CompanyProfileMapper mapper, CompanyPersonMapper personMapper,
WecomAccountMapper wecomMapper, DouyinAccountMapper douyinMapper,
DomainAssetMapper domainMapper, MerchantMapper merchantMapper) {
this.mapper = mapper; this.mapper = mapper;
this.personMapper = personMapper;
this.wecomMapper = wecomMapper;
this.douyinMapper = douyinMapper;
this.domainMapper = domainMapper;
this.merchantMapper = merchantMapper;
} }
/** /**
...@@ -62,6 +85,32 @@ public class CompanyProfileService { ...@@ -62,6 +85,32 @@ public class CompanyProfileService {
} }
/** /**
* 代码作用(白话):编辑一条仍有效的公司档案,只覆盖页面允许修改的业务字段。
* 关联文件:CompanyProfileController.java、CompanyProfileView.js。
* 关联逻辑(调用链/数据流):PUT 表单 -> 查找有效档案 -> 更新字段 -> Mapper.updateById -> 响应。
*/
public CompanyProfileResponse update(Long id, CompanyProfileSaveRequest request) {
CompanyProfileEntity entity = requireActive(id);
applyEditableFields(entity, request);
entity.setUpdateTime(LocalDateTime.now());
if (mapper.updateById(entity) != 1) throw new IllegalArgumentException("公司档案不存在或已删除");
return toResponse(entity);
}
/**
* 代码作用(白话):确认没有有效业务引用后把公司档案标记为已删除,保留历史记录。
* 关联文件:CompanyProfileController.java、各引用资产实体。
* 关联逻辑(调用链/数据流):DELETE -> 有效引用计数 -> deleteTime/updateTime -> Mapper.updateById。
*/
public void softDelete(Long id) {
CompanyProfileEntity entity = requireActive(id);
checkActiveReferences(id);
entity.setDeleteTime(System.currentTimeMillis());
entity.setUpdateTime(LocalDateTime.now());
if (mapper.updateById(entity) != 1) throw new IllegalArgumentException("公司档案不存在或已删除");
}
/**
* 代码作用(白话):组合“未删除”和关键词条件,保证公司档案页面只读取仍有效的数据。 * 代码作用(白话):组合“未删除”和关键词条件,保证公司档案页面只读取仍有效的数据。
* 关联文件:CompanyProfileEntity.java、CompanyProfilePageQuery.java。 * 关联文件:CompanyProfileEntity.java、CompanyProfilePageQuery.java。
* 关联逻辑(调用链/数据流):页面关键词 -> 查询条件 -> as_company_profile SQL where/order by。 * 关联逻辑(调用链/数据流):页面关键词 -> 查询条件 -> as_company_profile SQL where/order by。
...@@ -127,12 +176,46 @@ public class CompanyProfileService { ...@@ -127,12 +176,46 @@ public class CompanyProfileService {
} }
/** /**
* 代码作用(白话):把公司档案表单允许编辑的六个字段统一复制到实体,并清理首尾空格。
* 关联文件:CompanyProfileSaveRequest.java、CompanyProfileEntity.java。
* 关联逻辑(调用链/数据流):新增/编辑请求 -> 字段清理 -> 实体 -> 数据库。
*/
private void applyEditableFields(CompanyProfileEntity entity, CompanyProfileSaveRequest request) {
entity.setCompanyName(normalizeRequiredText(request.companyName(), "公司名称不能为空"));
entity.setShortName(normalizeOptionalText(request.shortName()));
entity.setUnifiedSocialCreditCode(normalizeOptionalText(request.unifiedSocialCreditCode()));
entity.setAddress(normalizeOptionalText(request.address()));
entity.setContactName(normalizeOptionalText(request.contactName()));
entity.setContactValue(normalizeOptionalText(request.contactValue()));
}
/** 代码作用(白话):读取一条未删除公司供编辑和删除共用。关联文件:CompanyProfileMapper.java。关联逻辑(调用链/数据流):路径 ID -> 有效记录查询 -> 实体或参数错误。 */
private CompanyProfileEntity requireActive(Long id) {
CompanyProfileEntity entity = mapper.selectOne(new LambdaQueryWrapper<CompanyProfileEntity>()
.eq(CompanyProfileEntity::getId, id).eq(CompanyProfileEntity::getDeleteTime, 0L));
if (entity == null) throw new IllegalArgumentException("公司档案不存在或已删除");
return entity;
}
/** 代码作用(白话):汇总仍引用公司的业务类型并阻止删除。关联文件:人员、企微、抖音、域名、商户实体。关联逻辑(调用链/数据流):公司 ID -> 各表有效引用计数 -> 错误提示或允许软删除。 */
private void checkActiveReferences(Long id) {
List<String> types = new ArrayList<>();
if (personMapper.selectCount(new LambdaQueryWrapper<CompanyPersonEntity>().eq(CompanyPersonEntity::getCompanyProfileId, id).eq(CompanyPersonEntity::getDeleteTime, 0L)) > 0) types.add("公司人员");
if (wecomMapper.selectCount(new LambdaQueryWrapper<WecomAccountEntity>().eq(WecomAccountEntity::getCompanyProfileId, id).eq(WecomAccountEntity::getDeleteTime, 0L)) > 0) types.add("企业微信资产");
if (douyinMapper.selectCount(new LambdaQueryWrapper<DouyinAccountEntity>().eq(DouyinAccountEntity::getCompanyProfileId, id).eq(DouyinAccountEntity::getDeleteTime, 0L)) > 0) types.add("抖音资产");
if (domainMapper.selectCount(new LambdaQueryWrapper<DomainAssetEntity>().eq(DomainAssetEntity::getCompanyProfileId, id).eq(DomainAssetEntity::getDeleteTime, 0L)) > 0) types.add("域名资产");
if (merchantMapper.selectCount(new LambdaQueryWrapper<MerchantEntity>().eq(MerchantEntity::getCompanyProfileId, id).eq(MerchantEntity::getDeleteTime, 0L)) > 0) types.add("商户");
if (!types.isEmpty()) throw new IllegalArgumentException("公司档案仍被" + String.join("、", types) + "引用,不能删除");
}
/**
* 代码作用(白话):从实体挑选公司档案页面允许展示的字段,隐藏内部 ID 和软删除标志。 * 代码作用(白话):从实体挑选公司档案页面允许展示的字段,隐藏内部 ID 和软删除标志。
* 关联文件:CompanyProfileEntity.java、CompanyProfileResponse.java。 * 关联文件:CompanyProfileEntity.java、CompanyProfileResponse.java。
* 关联逻辑(调用链/数据流):数据库实体 -> Response DTO -> ApiResponse -> Vue 表格。 * 关联逻辑(调用链/数据流):数据库实体 -> Response DTO -> ApiResponse -> Vue 表格。
*/ */
private CompanyProfileResponse toResponse(CompanyProfileEntity entity) { private CompanyProfileResponse toResponse(CompanyProfileEntity entity) {
return new CompanyProfileResponse( return new CompanyProfileResponse(
entity.getId(),
entity.getCompanyName(), entity.getCompanyName(),
entity.getShortName(), entity.getShortName(),
entity.getUnifiedSocialCreditCode(), entity.getUnifiedSocialCreditCode(),
......
...@@ -17,8 +17,9 @@ public class PagePermissionService { ...@@ -17,8 +17,9 @@ public class PagePermissionService {
public static final String WECOM = "reference-wecom"; public static final String WECOM = "reference-wecom";
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 COMPANY_PERSON = "company-person";
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, "公司档案", COMPANY_PERSON, "公司人员", 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 判定。 */
......
package com.xyw.console.asset.controller;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import com.xyw.console.asset.dto.CompanyPersonPageQuery;
import com.xyw.console.asset.service.CompanyPersonService;
import com.xyw.console.auth.PagePermissionService;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.AccessDeniedException;
/** 文件用途(白话):验证公司人员接口使用独立页面权限保护读写操作。 */
class CompanyPersonControllerTest {
/** 代码作用(白话):验证没有公司人员查看权限时在调用业务服务前拒绝分页请求。关联文件:CompanyPersonController.java、PagePermissionService.java。关联逻辑(调用链/数据流):GET -> company-person READ -> 拒绝。 */
@Test
void rejectsReadsWithoutCompanyPersonPermission() {
CompanyPersonService service=mock(CompanyPersonService.class); PagePermissionService permissions=mock(PagePermissionService.class);
doThrow(new AccessDeniedException("没有页面权限")).when(permissions).require(PagePermissionService.COMPANY_PERSON,"READ");
CompanyPersonController controller=new CompanyPersonController(service,permissions);
assertThrows(AccessDeniedException.class,()->controller.page(new CompanyPersonPageQuery(1,20,null,null,null)));
verify(permissions).require(PagePermissionService.COMPANY_PERSON,"READ");
}
}
...@@ -9,6 +9,8 @@ import static org.mockito.Mockito.when; ...@@ -9,6 +9,8 @@ import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
...@@ -27,7 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; ...@@ -27,7 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
class CompanyProfileControllerTest { class CompanyProfileControllerTest {
/** /**
* 代码作用(白话):验证公司档案 GET 接口返回完整可读字段但不返回内部 ID 或软删除标志。 * 代码作用(白话):验证公司档案 GET 接口返回编辑删除定位所需 ID 和可读字段,但不返回软删除标志。
* 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileResponse.java。 * 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileResponse.java。
* 关联逻辑(调用链/数据流):GET /api/company-profiles -> Controller -> Service -> JSON 表格数据。 * 关联逻辑(调用链/数据流):GET /api/company-profiles -> Controller -> Service -> JSON 表格数据。
*/ */
...@@ -47,7 +49,7 @@ class CompanyProfileControllerTest { ...@@ -47,7 +49,7 @@ class CompanyProfileControllerTest {
.andExpect(jsonPath("$.data.records[0].address", is("杭州市西湖区"))) .andExpect(jsonPath("$.data.records[0].address", is("杭州市西湖区")))
.andExpect(jsonPath("$.data.records[0].contactName", is("张三"))) .andExpect(jsonPath("$.data.records[0].contactName", is("张三")))
.andExpect(jsonPath("$.data.records[0].contactValue", is("13812345678"))) .andExpect(jsonPath("$.data.records[0].contactValue", is("13812345678")))
.andExpect(jsonPath("$.data.records[0].id").doesNotExist()) .andExpect(jsonPath("$.data.records[0].id", is(10)))
.andExpect(jsonPath("$.data.records[0].deleteTime").doesNotExist()); .andExpect(jsonPath("$.data.records[0].deleteTime").doesNotExist());
} }
...@@ -69,7 +71,7 @@ class CompanyProfileControllerTest { ...@@ -69,7 +71,7 @@ class CompanyProfileControllerTest {
} }
/** /**
* 代码作用(白话):验证拥有编辑权限时可以通过 POST 新增公司档案,且响应不含内部 ID。 * 代码作用(白话):验证拥有编辑权限时可以通过 POST 新增公司档案,并返回后续编辑可用的 ID。
* 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileView.js。 * 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileView.js。
* 关联逻辑(调用链/数据流):新增弹窗 -> POST -> EDIT 权限校验 -> Service.create -> JSON 响应。 * 关联逻辑(调用链/数据流):新增弹窗 -> POST -> EDIT 权限校验 -> Service.create -> JSON 响应。
*/ */
...@@ -86,7 +88,7 @@ class CompanyProfileControllerTest { ...@@ -86,7 +88,7 @@ class CompanyProfileControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.code", is(200))) .andExpect(jsonPath("$.code", is(200)))
.andExpect(jsonPath("$.data.companyName", is("示例科技有限公司"))) .andExpect(jsonPath("$.data.companyName", is("示例科技有限公司")))
.andExpect(jsonPath("$.data.id").doesNotExist()) .andExpect(jsonPath("$.data.id", is(10)))
.andExpect(jsonPath("$.data.deleteTime").doesNotExist()); .andExpect(jsonPath("$.data.deleteTime").doesNotExist());
verify(service).create(any()); verify(service).create(any());
} }
...@@ -110,6 +112,18 @@ class CompanyProfileControllerTest { ...@@ -110,6 +112,18 @@ class CompanyProfileControllerTest {
verify(service, org.mockito.Mockito.never()).create(any()); verify(service, org.mockito.Mockito.never()).create(any());
} }
/** 代码作用(白话):验证编辑和删除都要求公司档案编辑权限并调用对应服务。关联文件:CompanyProfileController.java、CompanyProfileService.java。关联逻辑(调用链/数据流):PUT/DELETE -> EDIT 权限 -> Service。 */
@Test
void updatesAndDeletesCompanyProfileWithEditPermission() throws Exception {
CompanyProfileService service = mock(CompanyProfileService.class);
when(service.update(org.mockito.ArgumentMatchers.eq(10L), any())).thenReturn(response().records().get(0));
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompanyProfileController(service, mock(PagePermissionService.class))).build();
mockMvc.perform(put("/api/company-profiles/10").contentType(org.springframework.http.MediaType.APPLICATION_JSON).content("{\"companyName\":\"新公司\"}"))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.id", is(10)));
mockMvc.perform(delete("/api/company-profiles/10")).andExpect(status().isOk());
verify(service).softDelete(10L);
}
/** /**
* 代码作用(白话):提供 Controller JSON 断言所需的只读公司档案分页结果。 * 代码作用(白话):提供 Controller JSON 断言所需的只读公司档案分页结果。
* 关联文件:CompanyProfilePageResponse.java、CompanyProfileController.java。 * 关联文件:CompanyProfilePageResponse.java、CompanyProfileController.java。
...@@ -117,7 +131,7 @@ class CompanyProfileControllerTest { ...@@ -117,7 +131,7 @@ class CompanyProfileControllerTest {
*/ */
private CompanyProfilePageResponse response() { private CompanyProfilePageResponse response() {
return new CompanyProfilePageResponse(List.of(new CompanyProfileResponse( return new CompanyProfilePageResponse(List.of(new CompanyProfileResponse(
"示例科技有限公司", "示例科技", "91330100TEST000001", "杭州市西湖区", "张三", "13812345678", 10L, "示例科技有限公司", "示例科技", "91330100TEST000001", "杭州市西湖区", "张三", "13812345678",
LocalDateTime.of(2026, 8, 5, 10, 0), LocalDateTime.of(2026, 8, 5, 11, 0))), 1L, 1, 20); LocalDateTime.of(2026, 8, 5, 10, 0), LocalDateTime.of(2026, 8, 5, 11, 0))), 1L, 1, 20);
} }
} }
package com.xyw.console.asset.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.xyw.console.asset.dto.CompanyPersonSaveRequest;
import com.xyw.console.asset.dto.CompanyPersonResponse;
import com.xyw.console.asset.dto.CompanyPersonPageQuery;
import com.xyw.console.asset.entity.CompanyPersonEntity;
import com.xyw.console.asset.entity.CompanyProfileEntity;
import com.xyw.console.asset.mapper.AssetDeviceMapper;
import com.xyw.console.asset.mapper.CompanyPersonMapper;
import com.xyw.console.asset.mapper.CompanyProfileMapper;
import com.xyw.console.asset.mapper.DouyinAccountMapper;
import com.xyw.console.asset.mapper.WechatAccountMapper;
import com.xyw.console.asset.mapper.WecomAccountMapper;
import java.time.LocalDateTime;
import java.util.List;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
/** 文件用途(白话):验证公司人员保存规则、公司名称解析和引用安全删除。 */
class CompanyPersonServiceTest {
/**
* 代码作用(白话):验证历史人员没有所属公司时仍能正常分页,并以空公司名称返回而不是抛出服务器错误。
* 关联文件:CompanyPersonService.java、CompanyPersonPageResponse.java。
* 关联逻辑(调用链/数据流):GET 分页 -> companyProfileId=null -> 不查询空关联键 -> 页面记录。
*/
@Test
void pagesPersonWithoutCompanyUsingEmptyDisplayName() {
CompanyPersonMapper mapper = mock(CompanyPersonMapper.class);
Page<CompanyPersonEntity> page = new Page<>(1, 20);
CompanyPersonEntity person = new CompanyPersonEntity();
person.setId(3L); person.setPersonName("张三"); person.setEmploymentStatus("在职"); person.setDeleteTime(0L);
page.setRecords(List.of(person)); page.setTotal(1L);
when(mapper.selectPage(any(), any())).thenReturn(page);
CompanyPersonResponse result = service(mapper, mock(CompanyProfileMapper.class))
.page(new CompanyPersonPageQuery(1, 20, null, null, null)).records().get(0);
assertNull(result.companyProfileName());
assertEquals("张三", result.personName());
}
/**
* 代码作用(白话):验证新增人员默认在职、所属公司可空,并由服务端初始化审计字段。
* 关联文件:CompanyPersonService.java、CompanyPersonSaveRequest.java。
* 关联逻辑(调用链/数据流):POST 表单 -> Service.create -> 人员实体 -> Mapper.insert。
*/
@Test
void createsActivePersonWithoutCompanyByDefault() {
CompanyPersonMapper mapper = mock(CompanyPersonMapper.class);
when(mapper.insert(org.mockito.ArgumentMatchers.<CompanyPersonEntity>any())).thenReturn(1);
CompanyPersonResponse result = service(mapper, mock(CompanyProfileMapper.class))
.create(new CompanyPersonSaveRequest(null, " 张三 ", null, null));
assertEquals("张三", result.personName());
assertEquals("在职", result.employmentStatus());
assertNull(result.companyProfileId());
assertNull(result.resignedAt());
}
/**
* 代码作用(白话):验证离职人员必须填写离职时间,防止状态与时间互相矛盾。
* 关联文件:CompanyPersonService.java、CompanyPersonSaveRequest.java。
* 关联逻辑(调用链/数据流):离职表单 -> 状态校验 -> 参数错误 -> 不写数据库。
*/
@Test
void requiresResignedAtForResignedPerson() {
CompanyPersonMapper mapper = mock(CompanyPersonMapper.class);
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () ->
service(mapper, mock(CompanyProfileMapper.class)).create(
new CompanyPersonSaveRequest(null, "张三", "离职", null)));
assertEquals("离职人员必须填写离职时间", error.getMessage());
verify(mapper, never()).insert(org.mockito.ArgumentMatchers.<CompanyPersonEntity>any());
}
/**
* 代码作用(白话):验证选择所属公司时只接受有效公司,并优先返回公司简称供页面展示。
* 关联文件:CompanyPersonService.java、CompanyProfileMapper.java。
* 关联逻辑(调用链/数据流):公司 ID -> 有效公司查询 -> 简称/全称 -> 人员响应。
*/
@Test
void resolvesSelectedCompanyUsingShortNameFirst() {
CompanyPersonMapper mapper = mock(CompanyPersonMapper.class);
CompanyProfileMapper companies = mock(CompanyProfileMapper.class);
CompanyProfileEntity company = new CompanyProfileEntity();
company.setId(7L); company.setCompanyName("示例科技有限公司"); company.setShortName("示例科技"); company.setDeleteTime(0L);
when(companies.selectOne(any())).thenReturn(company);
when(mapper.insert(org.mockito.ArgumentMatchers.<CompanyPersonEntity>any())).thenReturn(1);
CompanyPersonResponse result = service(mapper, companies).create(
new CompanyPersonSaveRequest(7L, "张三", "离职", LocalDateTime.of(2026, 8, 12, 9, 30)));
assertEquals("示例科技", result.companyProfileName());
}
/**
* 代码作用(白话):验证人员仍被设备引用时拒绝删除,避免设备使用人名称断链。
* 关联文件:CompanyPersonService.java、AssetDeviceMapper.java。
* 关联逻辑(调用链/数据流):DELETE -> 设备引用计数 -> 参数错误 -> 保留人员。
*/
@Test
void rejectsDeleteWhenDeviceReferencesPerson() {
CompanyPersonMapper mapper = mock(CompanyPersonMapper.class);
CompanyPersonEntity person = new CompanyPersonEntity(); person.setId(3L); person.setDeleteTime(0L);
when(mapper.selectOne(any())).thenReturn(person);
AssetDeviceMapper devices = mock(AssetDeviceMapper.class);
when(devices.selectCount(any())).thenReturn(1L);
CompanyPersonService service = new CompanyPersonService(mapper, mock(CompanyProfileMapper.class), devices,
mock(WecomAccountMapper.class), mock(WechatAccountMapper.class), mock(DouyinAccountMapper.class));
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.softDelete(3L));
assertEquals("公司人员仍被设备资产管理引用,不能删除", error.getMessage());
verify(mapper, never()).updateById(org.mockito.ArgumentMatchers.<CompanyPersonEntity>any());
}
/**
* 代码作用(白话):构造默认没有资产引用的人员服务供保存规则测试使用。
* 关联文件:CompanyPersonService.java、人员相关 Mapper。
* 关联逻辑(调用链/数据流):测试 -> service 工厂 -> 被测 Service。
*/
private CompanyPersonService service(CompanyPersonMapper mapper, CompanyProfileMapper companies) {
return new CompanyPersonService(mapper, companies, mock(AssetDeviceMapper.class), mock(WecomAccountMapper.class),
mock(WechatAccountMapper.class), mock(DouyinAccountMapper.class));
}
}
...@@ -15,6 +15,11 @@ import com.xyw.console.asset.dto.CompanyProfileResponse; ...@@ -15,6 +15,11 @@ import com.xyw.console.asset.dto.CompanyProfileResponse;
import com.xyw.console.asset.dto.CompanyProfileSaveRequest; import com.xyw.console.asset.dto.CompanyProfileSaveRequest;
import com.xyw.console.asset.entity.CompanyProfileEntity; import com.xyw.console.asset.entity.CompanyProfileEntity;
import com.xyw.console.asset.mapper.CompanyProfileMapper; import com.xyw.console.asset.mapper.CompanyProfileMapper;
import com.xyw.console.asset.mapper.CompanyPersonMapper;
import com.xyw.console.asset.mapper.DomainAssetMapper;
import com.xyw.console.asset.mapper.DouyinAccountMapper;
import com.xyw.console.asset.mapper.MerchantMapper;
import com.xyw.console.asset.mapper.WecomAccountMapper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -34,7 +39,7 @@ class CompanyProfileServiceTest { ...@@ -34,7 +39,7 @@ class CompanyProfileServiceTest {
page.setTotal(1L); page.setTotal(1L);
when(mapper.selectPage(any(), any())).thenReturn(page); when(mapper.selectPage(any(), any())).thenReturn(page);
CompanyProfilePageResponse result = new CompanyProfileService(mapper) CompanyProfilePageResponse result = service(mapper)
.page(new CompanyProfilePageQuery(null, null, null)); .page(new CompanyProfilePageQuery(null, null, null));
CompanyProfileResponse record = result.records().get(0); CompanyProfileResponse record = result.records().get(0);
...@@ -59,7 +64,7 @@ class CompanyProfileServiceTest { ...@@ -59,7 +64,7 @@ class CompanyProfileServiceTest {
void createsCompanyProfileWithServerManagedAuditFields() { void createsCompanyProfileWithServerManagedAuditFields() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class); CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
when(mapper.insert(org.mockito.ArgumentMatchers.<CompanyProfileEntity>any())).thenReturn(1); when(mapper.insert(org.mockito.ArgumentMatchers.<CompanyProfileEntity>any())).thenReturn(1);
CompanyProfileService service = new CompanyProfileService(mapper); CompanyProfileService service = service(mapper);
CompanyProfileResponse result = service.create(new CompanyProfileSaveRequest( CompanyProfileResponse result = service.create(new CompanyProfileSaveRequest(
" 示例科技有限公司 ", " 示例科技 ", " 91330100TEST000001 ", " 杭州市西湖区 ", " 张三 ", " 13812345678 ")); " 示例科技有限公司 ", " 示例科技 ", " 91330100TEST000001 ", " 杭州市西湖区 ", " 张三 ", " 13812345678 "));
...@@ -88,13 +93,64 @@ class CompanyProfileServiceTest { ...@@ -88,13 +93,64 @@ class CompanyProfileServiceTest {
void rejectsBlankCompanyNameBeforeInsert() { void rejectsBlankCompanyNameBeforeInsert() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class); CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
assertThrows(IllegalArgumentException.class, () -> new CompanyProfileService(mapper) assertThrows(IllegalArgumentException.class, () -> service(mapper)
.create(new CompanyProfileSaveRequest(" ", null, null, null, null, null))); .create(new CompanyProfileSaveRequest(" ", null, null, null, null, null)));
verify(mapper, org.mockito.Mockito.never()).insert(org.mockito.ArgumentMatchers.<CompanyProfileEntity>any()); verify(mapper, org.mockito.Mockito.never()).insert(org.mockito.ArgumentMatchers.<CompanyProfileEntity>any());
} }
/** /**
* 代码作用(白话):验证编辑会保留原记录主键和创建时间,只更新业务字段与更新时间。
* 关联文件:CompanyProfileService.java、CompanyProfileSaveRequest.java。
* 关联逻辑(调用链/数据流):PUT 表单 -> Service.update -> 原实体更新 -> Mapper.updateById。
*/
@Test
void updatesAnActiveCompanyProfile() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
CompanyProfileEntity existing = companyProfile();
when(mapper.selectOne(any())).thenReturn(existing);
when(mapper.updateById(existing)).thenReturn(1);
CompanyProfileResponse result = service(mapper).update(10L,
new CompanyProfileSaveRequest(" 新名称 ", " 新简称 ", null, null, null, null));
assertEquals(10L, result.id());
assertEquals("新名称", result.companyName());
assertEquals("新简称", result.shortName());
verify(mapper).updateById(existing);
}
/**
* 代码作用(白话):验证公司仍有有效人员引用时不能软删除,避免人员记录失去所属公司名称。
* 关联文件:CompanyProfileService.java、CompanyPersonMapper.java。
* 关联逻辑(调用链/数据流):DELETE -> 引用计数 -> 参数错误 -> 不更新 deleteTime。
*/
@Test
void rejectsDeleteWhenActivePeopleReferenceCompany() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
CompanyPersonMapper people = mock(CompanyPersonMapper.class);
when(mapper.selectOne(any())).thenReturn(companyProfile());
when(people.selectCount(any())).thenReturn(1L);
CompanyProfileService service = new CompanyProfileService(mapper, people, mock(WecomAccountMapper.class),
mock(DouyinAccountMapper.class), mock(DomainAssetMapper.class), mock(MerchantMapper.class));
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.softDelete(10L));
assertEquals("公司档案仍被公司人员引用,不能删除", error.getMessage());
verify(mapper, org.mockito.Mockito.never()).updateById(org.mockito.ArgumentMatchers.<CompanyProfileEntity>any());
}
/**
* 代码作用(白话):为公司档案测试构造一套默认无引用的业务服务,减少各测试重复创建 Mapper。
* 关联文件:CompanyProfileService.java、各资产 Mapper。
* 关联逻辑(调用链/数据流):测试 -> service 工厂 -> 被测 Service。
*/
private CompanyProfileService service(CompanyProfileMapper mapper) {
return new CompanyProfileService(mapper, mock(CompanyPersonMapper.class), mock(WecomAccountMapper.class),
mock(DouyinAccountMapper.class), mock(DomainAssetMapper.class), mock(MerchantMapper.class));
}
/**
* 代码作用(白话):构造一条完整的有效公司档案,供列表字段映射测试使用。 * 代码作用(白话):构造一条完整的有效公司档案,供列表字段映射测试使用。
* 关联文件:CompanyProfileEntity.java、CompanyProfileService.java。 * 关联文件:CompanyProfileEntity.java、CompanyProfileService.java。
* 关联逻辑(调用链/数据流):测试实体 -> Mapper 分页结果 -> Service 转换 -> Response 断言。 * 关联逻辑(调用链/数据流):测试实体 -> Mapper 分页结果 -> Service 转换 -> Response 断言。
......
...@@ -30,5 +30,5 @@ export default { ...@@ -30,5 +30,5 @@ export default {
return { authState, logout, canVisit, isAdministrator, retryConnection }; return { authState, logout, canVisit, isAdministrator, retryConnection };
}, },
template: `<main v-if="authState.connectionError" class="auth-network-error" role="alert"><section class="auth-network-error__panel"><h1>网络连接异常</h1><p>暂时无法确认登录状态,请检查网络后重试。</p><button type="button" @click="retryConnection">重新连接</button></section></main><main v-else-if="!authState.ready" class="auth-loading" role="status" aria-live="polite">正在验证登录状态…</main><RouterView v-else-if="$route.path === '/login'" /><div v-else class="app-shell"><aside class="sidebar"><h1>学有为资产后台</h1><nav aria-label="主导航"><RouterLink v-if="canVisit('overview')" to="/overview">总览</RouterLink><RouterLink v-if="canVisit('domain')" to="/domain">域名资料</RouterLink><RouterLink v-if="canVisit('company-profile')" to="/company-profiles">公司档案</RouterLink><RouterLink v-if="canVisit('reference-wecom')" to="/reference/wecom">企业微信资产</RouterLink><RouterLink v-if="canVisit('phone-assets')" to="/phone-assets">手机号码管理</RouterLink><RouterLink v-if="isAdministrator()" to="/device-assets">设备资产管理</RouterLink><RouterLink v-if="canVisit('alerts')" to="/alerts">提醒中心</RouterLink><RouterLink v-if="isAdministrator()" class="settings-link" to="/settings/users-permissions"><span aria-hidden="true">⚙</span> 账号与权限</RouterLink></nav><button class="logout-button" @click="logout">退出登录</button></aside><main class="content"><RouterView /></main></div>` template: `<main v-if="authState.connectionError" class="auth-network-error" role="alert"><section class="auth-network-error__panel"><h1>网络连接异常</h1><p>暂时无法确认登录状态,请检查网络后重试。</p><button type="button" @click="retryConnection">重新连接</button></section></main><main v-else-if="!authState.ready" class="auth-loading" role="status" aria-live="polite">正在验证登录状态…</main><RouterView v-else-if="$route.path === '/login'" /><div v-else class="app-shell"><aside class="sidebar"><h1>学有为资产后台</h1><nav aria-label="主导航"><RouterLink v-if="canVisit('overview')" to="/overview">总览</RouterLink><RouterLink v-if="canVisit('domain')" to="/domain">域名资料</RouterLink><RouterLink v-if="canVisit('company-profile')" to="/company-profiles">公司档案</RouterLink><RouterLink v-if="canVisit('company-person')" to="/company-persons">公司人员</RouterLink><RouterLink v-if="canVisit('reference-wecom')" to="/reference/wecom">企业微信资产</RouterLink><RouterLink v-if="canVisit('phone-assets')" to="/phone-assets">手机号码管理</RouterLink><RouterLink v-if="isAdministrator()" to="/device-assets">设备资产管理</RouterLink><RouterLink v-if="canVisit('alerts')" to="/alerts">提醒中心</RouterLink><RouterLink v-if="isAdministrator()" class="settings-link" to="/settings/users-permissions"><span aria-hidden="true">⚙</span> 账号与权限</RouterLink></nav><button class="logout-button" @click="logout">退出登录</button></aside><main class="content"><RouterView /></main></div>`
}; };
import { computed,onMounted,reactive,ref,watch } from 'vue/dist/vue.esm-bundler.js';
import { ElMessage,ElMessageBox } from 'element-plus';
import { authState } from '../auth/auth-store.js';
import { createCompanyPerson,deleteCompanyPerson,listCompanyPersons,searchPersonCompanies,updateCompanyPerson } from './company-person-api-client.js';
/** 文件用途(白话):显示公司人员筛选、分页表格及复用现有样式的新增编辑弹窗。 */
export default { setup(){
const loading=ref(false),saving=ref(false),records=ref([]),total=ref(0),dialogVisible=ref(false),editingId=ref(null),companyOptions=ref([]);
const filters=reactive({page:1,size:20,keyword:'',companyProfileId:null,employmentStatus:''}); const form=reactive({companyProfileId:null,personName:'',employmentStatus:'在职',resignedAt:null}); const canEdit=computed(()=>authState.user?.pagePermissions?.['company-person']==='EDIT'); let searchTimer;
/** 代码作用(白话):读取当前筛选页并在删空末页时回退。关联文件:company-person-api-client.js、CompanyPersonService.java。关联逻辑(调用链/数据流):进入/筛选/保存 -> GET -> 表格。 */
async function loadPage(){loading.value=true;try{const result=await listCompanyPersons(filters);records.value=result.records;total.value=result.total;if(!result.records.length&&filters.page>1){filters.page=Math.max(1,Math.ceil(result.total/filters.size));await loadPage();}}catch(error){ElMessage.error(error.message);}finally{loading.value=false;}}
/** 代码作用(白话):恢复人员新增表单默认值。关联文件:CompanyPersonSaveRequest.java。关联逻辑(调用链/数据流):新增按钮 -> 空表单 -> POST。 */ function resetForm(){Object.assign(form,{companyProfileId:null,personName:'',employmentStatus:'在职',resignedAt:null});companyOptions.value=[];}
/** 代码作用(白话):打开新增人员弹窗。关联文件:CompanyPersonView.js。关联逻辑(调用链/数据流):新增按钮 -> 默认表单 -> 弹窗。 */ function openCreate(){editingId.value=null;resetForm();dialogVisible.value=true;}
/** 代码作用(白话):把选中人员复制到编辑弹窗并保留已选公司标签。关联文件:CompanyPersonResponse.java。关联逻辑(调用链/数据流):编辑按钮 -> 行数据 -> PUT 表单。 */ function openEdit(row){editingId.value=row.id;Object.assign(form,{companyProfileId:row.companyProfileId,personName:row.personName||'',employmentStatus:row.employmentStatus||'在职',resignedAt:row.resignedAt||null});companyOptions.value=row.companyProfileId?[{id:row.companyProfileId,companyName:row.companyProfileName,shortName:row.companyProfileName}]:[];dialogVisible.value=true;}
/** 代码作用(白话):校验姓名和离职时间后执行新增或编辑。关联文件:company-person-api-client.js、CompanyPersonService.java。关联逻辑(调用链/数据流):确认保存 -> POST/PUT -> 成功提示 -> 第一页刷新。 */ async function submitSave(){if(!form.personName.trim()){ElMessage.warning('请填写人员姓名');return;}if(form.employmentStatus==='离职'&&!form.resignedAt){ElMessage.warning('请选择离职时间');return;}saving.value=true;try{const payload={...form,personName:form.personName.trim()};if(editingId.value===null)await createCompanyPerson(payload);else await updateCompanyPerson(editingId.value,payload);ElMessage.success(editingId.value===null?'新增成功':'修改成功');dialogVisible.value=false;filters.page=1;await loadPage();}catch(error){ElMessage.error(error.message);}finally{saving.value=false;}}
/** 代码作用(白话):二次确认后请求引用安全删除人员。关联文件:CompanyPersonService.java、company-person-api-client.js。关联逻辑(调用链/数据流):删除按钮 -> 确认 -> DELETE -> 列表或错误。 */ async function confirmDelete(row){try{await ElMessageBox.confirm(`确认删除“${row.personName}”吗?`,'删除公司人员',{confirmButtonText:'确认删除',cancelButtonText:'取消',type:'warning'});await deleteCompanyPerson(row.id);ElMessage.success('删除成功');await loadPage();}catch(error){if(error!=='cancel'&&error!=='close')ElMessage.error(error.message||'删除失败');}}
/** 代码作用(白话):远程搜索所属公司并供筛选和表单共用。关联文件:company-person-api-client.js、CompanyProfileEntity.java。关联逻辑(调用链/数据流):输入名称/简称 -> lookup -> 下拉。 */ async function loadCompanies(keyword){try{companyOptions.value=await searchPersonCompanies(keyword);}catch(error){ElMessage.error(error.message);}}
/** 代码作用(白话):防抖搜索人员姓名。关联文件:CompanyPersonView.js。关联逻辑(调用链/数据流):输入 -> 300ms -> 第一页查询。 */ function scheduleSearch(){window.clearTimeout(searchTimer);searchTimer=window.setTimeout(submitSearch,300);}
/** 代码作用(白话):从第一页按当前条件立即查询。关联文件:CompanyPersonPageQuery.java。关联逻辑(调用链/数据流):筛选变化 -> page=1 -> GET。 */ function submitSearch(){filters.page=1;loadPage();}
/** 代码作用(白话):清空全部人员筛选并重读列表。关联文件:CompanyPersonView.js。关联逻辑(调用链/数据流):重置 -> 默认 filters -> GET。 */ function resetSearch(){window.clearTimeout(searchTimer);Object.assign(filters,{page:1,size:20,keyword:'',companyProfileId:null,employmentStatus:''});loadPage();}
/** 代码作用(白话):切换页码后读取对应人员。关联文件:AppPagination.js。关联逻辑(调用链/数据流):分页器 -> page -> GET。 */ function changePage(page){filters.page=page;loadPage();}
/** 代码作用(白话):切换每页数量并回到第一页。关联文件:AppPagination.js。关联逻辑(调用链/数据流):分页器 -> size/page -> GET。 */ function changePageSize(size){filters.size=size;filters.page=1;loadPage();}
/** 代码作用(白话):统一显示空字段。关联文件:CompanyPersonResponse.java。关联逻辑(调用链/数据流):响应字段 -> 文本单元格。 */ function formatValue(value){return value||'—';}
/** 代码作用(白话):把 ISO 时间转换为本地可读时间。关联文件:CompanyPersonResponse.java。关联逻辑(调用链/数据流):时间字符串 -> 本地文本 -> 表格。 */ function formatDate(value){return value?new Date(value).toLocaleString('zh-CN'):'—';}
/** 代码作用(白话):在职状态恢复时立即清空离职时间,避免提交矛盾数据。关联文件:CompanyPersonSaveRequest.java。关联逻辑(调用链/数据流):状态变化 -> resignedAt 清空 -> 保存。 */ watch(()=>form.employmentStatus,status=>{if(status==='在职')form.resignedAt=null;});
onMounted(loadPage);return{canEdit,changePage,changePageSize,companyOptions,confirmDelete,dialogVisible,editingId,filters,form,formatDate,formatValue,loadCompanies,loading,openCreate,openEdit,records,resetSearch,saving,scheduleSearch,submitSave,submitSearch,total};
},template:`<section class="phone-asset-list-page company-person-page"><header class="phone-asset-list-page__header"><div><h2>公司人员</h2></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增公司人员</el-button></header><section class="phone-asset-list-page__panel phone-asset-list-page__search"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.keyword" placeholder="人员姓名" clearable @input="scheduleSearch"/><el-select v-model="filters.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="所属公司" @change="submitSearch"><el-option v-for="item in companyOptions" :key="item.id" :label="item.shortName||item.companyName" :value="item.id"/></el-select><el-select v-model="filters.employmentStatus" clearable placeholder="在职状态" @change="submitSearch"><el-option label="在职" value="在职"/><el-option label="离职" value="离职"/></el-select><el-button @click="resetSearch">重置</el-button></el-form></section><section class="phone-asset-list-page__panel phone-asset-list-page__table"><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid"><el-table-column prop="personName" label="人员姓名" min-width="140"/><el-table-column label="所属公司" min-width="180"><template #default="{row}">{{formatValue(row.companyProfileName)}}</template></el-table-column><el-table-column prop="employmentStatus" label="在职状态" min-width="110"/><el-table-column label="离职时间" min-width="180"><template #default="{row}">{{formatDate(row.resignedAt)}}</template></el-table-column><el-table-column label="创建时间" min-width="180"><template #default="{row}">{{formatDate(row.createTime)}}</template></el-table-column><el-table-column label="更新时间" min-width="180"><template #default="{row}">{{formatDate(row.updateTime)}}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120" fixed="right"><template #default="{row}"><span class="phone-asset-list-page__actions"><el-button link @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="confirmDelete(row)">删除</el-button></span></template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize"/></section><el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId===null?'新增公司人员':'编辑公司人员'" width="560px" :close-on-click-modal="false"><el-form class="phone-asset-modal__form" label-width="112px" @submit.prevent="submitSave"><el-form-item class="phone-asset-modal__form-row" label="人员姓名" required><el-input v-model="form.personName" maxlength="64" placeholder="请输入人员姓名"/></el-form-item><el-form-item class="phone-asset-modal__form-row" label="所属公司"><el-select v-model="form.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="输入公司名称或简称搜索" style="width:100%"><el-option v-for="item in companyOptions" :key="item.id" :label="item.shortName||item.companyName" :value="item.id"/></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="在职状态" required><el-radio-group v-model="form.employmentStatus"><el-radio value="在职">在职</el-radio><el-radio value="离职">离职</el-radio></el-radio-group></el-form-item><el-form-item v-if="form.employmentStatus==='离职'" class="phone-asset-modal__form-row" label="离职时间" required><el-date-picker v-model="form.resignedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择离职时间" style="width:100%"/></el-form-item></el-form><template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="saving" @click="submitSave">{{editingId===null?'确认保存':'保存修改'}}</el-button></template></el-dialog></section>`};
import { csrfHeadersFor } from '../auth/auth-api-client.js';
/** 文件用途(白话):集中请求公司人员分页、公司搜索及增删改接口。 */
/** 代码作用(白话):发送人员模块请求并统一解包或抛出可读错误。关联文件:CompanyPersonView.js、CompanyPersonController.java。关联逻辑(调用链/数据流):页面操作 -> fetch -> ApiResponse -> 页面状态。 */
async function request(path,options={}){const headers=await csrfHeadersFor(path,(options.method||'GET').toUpperCase());const response=await fetch(path,{credentials:'include',headers:{'Content-Type':'application/json',...headers},...options});const payload=await response.json();if(!response.ok||payload.code!==200)throw new Error(payload.message||'公司人员请求失败');return payload.data;}
/** 代码作用(白话):把人员筛选和分页状态拼成查询地址。关联文件:CompanyPersonView.js、CompanyPersonPageQuery.java。关联逻辑(调用链/数据流):筛选表单 -> URL 参数 -> GET 分页。 */
export function listCompanyPersons(query){const params=new URLSearchParams();Object.entries(query).forEach(([key,value])=>{if(value!==null&&value!==undefined&&value!==''&&value!=='ALL')params.set(key,value);});return request(`/api/company-persons?${params.toString()}`);}
/** 代码作用(白话):按公司名称或简称获取远程下拉选项。关联文件:CompanyPersonView.js、CompanyPersonController.java。关联逻辑(调用链/数据流):输入关键词 -> lookup GET -> 公司选项。 */
export function searchPersonCompanies(keyword){return request(`/api/company-persons/lookups/company-profiles?keyword=${encodeURIComponent(keyword||'')}`);}
/** 代码作用(白话):提交新增人员表单。关联文件:CompanyPersonView.js、CompanyPersonService.java。关联逻辑(调用链/数据流):新增弹窗 -> POST -> 列表刷新。 */
export function createCompanyPerson(form){return request('/api/company-persons',{method:'POST',body:JSON.stringify(form)});}
/** 代码作用(白话):提交指定人员的编辑表单。关联文件:CompanyPersonView.js、CompanyPersonService.java。关联逻辑(调用链/数据流):编辑弹窗 -> PUT /{id} -> 列表刷新。 */
export function updateCompanyPerson(id,form){return request(`/api/company-persons/${id}`,{method:'PUT',body:JSON.stringify(form)});}
/** 代码作用(白话):请求引用检查并软删除指定人员。关联文件:CompanyPersonView.js、CompanyPersonService.java。关联逻辑(调用链/数据流):删除确认 -> DELETE /{id} -> 列表刷新。 */
export function deleteCompanyPerson(id){return request(`/api/company-persons/${id}`,{method:'DELETE'});}
import { computed, onMounted, reactive, ref } from 'vue/dist/vue.esm-bundler.js'; import { computed, onMounted, reactive, ref } from 'vue/dist/vue.esm-bundler.js';
import { ElMessage } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { authState } from '../auth/auth-store.js'; import { authState } from '../auth/auth-store.js';
import { createCompanyProfile, listCompanyProfiles } from './company-profile-api-client.js'; import { createCompanyProfile, deleteCompanyProfile, listCompanyProfiles, updateCompanyProfile } from './company-profile-api-client.js';
/** 文件用途(白话):显示可搜索、可分页、可新增的公司档案表格,并隐藏内部 ID 与审计字段编辑入口。 */ /** 文件用途(白话):显示可搜索、可分页、可新增的公司档案表格,并隐藏内部 ID 与审计字段编辑入口。 */
export default { export default {
...@@ -16,6 +16,7 @@ export default { ...@@ -16,6 +16,7 @@ export default {
const records = ref([]); const records = ref([]);
const total = ref(0); const total = ref(0);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const editingId = ref(null);
const filters = reactive({ page: 1, size: 20, keyword: '' }); const filters = reactive({ page: 1, size: 20, keyword: '' });
const form = reactive({ companyName: '', shortName: '', unifiedSocialCreditCode: '', address: '', contactName: '', contactValue: '' }); const form = reactive({ companyName: '', shortName: '', unifiedSocialCreditCode: '', address: '', contactName: '', contactValue: '' });
const canEdit = computed(() => authState.user?.pagePermissions?.['company-profile'] === 'EDIT'); const canEdit = computed(() => authState.user?.pagePermissions?.['company-profile'] === 'EDIT');
...@@ -60,9 +61,13 @@ export default { ...@@ -60,9 +61,13 @@ export default {
*/ */
function openCreate() { function openCreate() {
resetForm(); resetForm();
editingId.value = null;
dialogVisible.value = true; dialogVisible.value = true;
} }
/** 代码作用(白话):把表格记录复制到复用弹窗中供用户修改。关联文件:CompanyProfileView.js、company-profile-api-client.js。关联逻辑(调用链/数据流):编辑按钮 -> 表格行副本 -> PUT 表单。 */
function openEdit(row) { editingId.value=row.id; Object.assign(form,{companyName:row.companyName||'',shortName:row.shortName||'',unifiedSocialCreditCode:row.unifiedSocialCreditCode||'',address:row.address||'',contactName:row.contactName||'',contactValue:row.contactValue||''}); dialogVisible.value=true; }
/** /**
* 代码作用(白话):检查公司名称必填后提交新增请求,成功时关闭弹窗并刷新列表第一页。 * 代码作用(白话):检查公司名称必填后提交新增请求,成功时关闭弹窗并刷新列表第一页。
* 关联文件:company-profile-api-client.js、CompanyProfileController.java、CompanyProfileService.java。 * 关联文件:company-profile-api-client.js、CompanyProfileController.java、CompanyProfileService.java。
...@@ -76,8 +81,9 @@ export default { ...@@ -76,8 +81,9 @@ export default {
} }
saving.value = true; saving.value = true;
try { try {
await createCompanyProfile({ ...form, companyName }); const payload={ ...form, companyName };
ElMessage.success('新增成功'); if(editingId.value===null) await createCompanyProfile(payload); else await updateCompanyProfile(editingId.value,payload);
ElMessage.success(editingId.value===null?'新增成功':'修改成功');
dialogVisible.value = false; dialogVisible.value = false;
filters.page = 1; filters.page = 1;
await loadPage(); await loadPage();
...@@ -88,6 +94,9 @@ export default { ...@@ -88,6 +94,9 @@ export default {
} }
} }
/** 代码作用(白话):二次确认后删除无有效引用的公司档案,并刷新当前列表。关联文件:company-profile-api-client.js、CompanyProfileService.java。关联逻辑(调用链/数据流):删除按钮 -> 确认框 -> DELETE -> 列表刷新或错误提示。 */
async function confirmDelete(row) { try { await ElMessageBox.confirm(`确认删除“${row.shortName||row.companyName}”吗?`,'删除公司档案',{confirmButtonText:'确认删除',cancelButtonText:'取消',type:'warning'}); await deleteCompanyProfile(row.id); ElMessage.success('删除成功'); await loadPage(); } catch(error) { if(error!=='cancel'&&error!=='close') ElMessage.error(error.message||'删除失败'); } }
/** /**
* 代码作用(白话):等待用户停止输入后再从第一页搜索,避免每个字符都发送一次请求。 * 代码作用(白话):等待用户停止输入后再从第一页搜索,避免每个字符都发送一次请求。
* 关联文件:CompanyProfileView.js、company-profile-api-client.js。 * 关联文件:CompanyProfileView.js、company-profile-api-client.js。
...@@ -150,14 +159,14 @@ export default { ...@@ -150,14 +159,14 @@ export default {
} }
onMounted(loadPage); onMounted(loadPage);
return { canEdit, changePage, changePageSize, dialogVisible, filters, form, formatValue, loading, openCreate, records, resetSearch, saving, scheduleSearch, submitCreate, submitSearch, total }; return { canEdit, changePage, changePageSize, confirmDelete, dialogVisible, editingId, filters, form, formatValue, loading, openCreate, openEdit, records, resetSearch, saving, scheduleSearch, submitCreate, submitSearch, total };
}, },
template: ` template: `
<section class="phone-asset-list-page company-profile-page"> <section class="phone-asset-list-page company-profile-page">
<header class="phone-asset-list-page__header"><div><h2>公司档案</h2></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增公司档案</el-button></header> <header class="phone-asset-list-page__header"><div><h2>公司档案</h2></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增公司档案</el-button></header>
<section class="phone-asset-list-page__panel phone-asset-list-page__search"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.keyword" placeholder="公司名称、简称、信用代码、地址、联系人或联系方式" clearable @input="scheduleSearch" @clear="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-button @click="resetSearch">重置</el-button></el-form></section> <section class="phone-asset-list-page__panel phone-asset-list-page__search"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.keyword" placeholder="公司名称、简称、信用代码、地址、联系人或联系方式" clearable @input="scheduleSearch" @clear="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-button @click="resetSearch">重置</el-button></el-form></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid company-profile-page__grid"><el-table-column label="公司名称" min-width="200"><template #default="{ row }">{{ formatValue(row.companyName) }}</template></el-table-column><el-table-column label="公司简称" min-width="150"><template #default="{ row }">{{ formatValue(row.shortName) }}</template></el-table-column><el-table-column label="统一社会信用代码" min-width="210"><template #default="{ row }">{{ formatValue(row.unifiedSocialCreditCode) }}</template></el-table-column><el-table-column label="地址" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ formatValue(row.address) }}</template></el-table-column><el-table-column label="联系人" min-width="130"><template #default="{ row }">{{ formatValue(row.contactName) }}</template></el-table-column><el-table-column label="联系方式" min-width="170"><template #default="{ row }">{{ formatValue(row.contactValue) }}</template></el-table-column><el-table-column label="创建时间" min-width="180"><template #default="{ row }">{{ formatValue(row.createTime) }}</template></el-table-column><el-table-column label="更新时间" min-width="180"><template #default="{ row }">{{ formatValue(row.updateTime) }}</template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize" /></section> <section class="phone-asset-list-page__panel phone-asset-list-page__table"><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid company-profile-page__grid"><el-table-column label="公司名称" min-width="200"><template #default="{ row }">{{ formatValue(row.companyName) }}</template></el-table-column><el-table-column label="公司简称" min-width="150"><template #default="{ row }">{{ formatValue(row.shortName) }}</template></el-table-column><el-table-column label="统一社会信用代码" min-width="210"><template #default="{ row }">{{ formatValue(row.unifiedSocialCreditCode) }}</template></el-table-column><el-table-column label="地址" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ formatValue(row.address) }}</template></el-table-column><el-table-column label="联系人" min-width="130"><template #default="{ row }">{{ formatValue(row.contactName) }}</template></el-table-column><el-table-column label="联系方式" min-width="170"><template #default="{ row }">{{ formatValue(row.contactValue) }}</template></el-table-column><el-table-column label="创建时间" min-width="180"><template #default="{ row }">{{ formatValue(row.createTime) }}</template></el-table-column><el-table-column label="更新时间" min-width="180"><template #default="{ row }">{{ formatValue(row.updateTime) }}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120" fixed="right"><template #default="{ row }"><span class="phone-asset-list-page__actions"><el-button link @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="confirmDelete(row)">删除</el-button></span></template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize" /></section>
<el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" title="新增公司档案" width="560px"> <el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId===null?'新增公司档案':'编辑公司档案'" width="560px">
<el-form class="phone-asset-modal__form" label-width="112px" @submit.prevent="submitCreate"> <el-form class="phone-asset-modal__form" label-width="112px" @submit.prevent="submitCreate">
<el-form-item class="phone-asset-modal__form-row" label="公司名称" required><el-input v-model="form.companyName" maxlength="100" autocomplete="off" placeholder="请输入公司名称" /></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="公司名称" required><el-input v-model="form.companyName" maxlength="100" autocomplete="off" placeholder="请输入公司名称" /></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="公司简称"><el-input v-model="form.shortName" maxlength="100" autocomplete="off" placeholder="请输入公司简称" /></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="公司简称"><el-input v-model="form.shortName" maxlength="100" autocomplete="off" placeholder="请输入公司简称" /></el-form-item>
...@@ -166,7 +175,7 @@ export default { ...@@ -166,7 +175,7 @@ export default {
<el-form-item class="phone-asset-modal__form-row" label="联系人"><el-input v-model="form.contactName" maxlength="100" autocomplete="off" placeholder="请输入联系人" /></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="联系人"><el-input v-model="form.contactName" maxlength="100" autocomplete="off" placeholder="请输入联系人" /></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="联系方式"><el-input v-model="form.contactValue" maxlength="100" autocomplete="off" placeholder="请输入联系方式" /></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="联系方式"><el-input v-model="form.contactValue" maxlength="100" autocomplete="off" placeholder="请输入联系方式" /></el-form-item>
</el-form> </el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">确认保存</el-button></template> <template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">{{ editingId===null?'确认保存':'保存修改' }}</el-button></template>
</el-dialog> </el-dialog>
</section> </section>
` `
......
...@@ -36,3 +36,9 @@ export function listCompanyProfiles(query) { ...@@ -36,3 +36,9 @@ export function listCompanyProfiles(query) {
export function createCompanyProfile(form) { export function createCompanyProfile(form) {
return request('/api/company-profiles', { method: 'POST', body: JSON.stringify(form) }); return request('/api/company-profiles', { method: 'POST', body: JSON.stringify(form) });
} }
/** 代码作用(白话):把编辑后的公司档案字段发送给指定记录。关联文件:CompanyProfileView.js、CompanyProfileController.java。关联逻辑(调用链/数据流):编辑弹窗 -> PUT /{id} -> Service.update -> 列表刷新。 */
export function updateCompanyProfile(id, form) { return request(`/api/company-profiles/${id}`, { method: 'PUT', body: JSON.stringify(form) }); }
/** 代码作用(白话):请求后端检查引用并软删除指定公司档案。关联文件:CompanyProfileView.js、CompanyProfileService.java。关联逻辑(调用链/数据流):删除确认 -> DELETE /{id} -> 引用检查/软删除 -> 列表刷新。 */
export function deleteCompanyProfile(id) { return request(`/api/company-profiles/${id}`, { method: 'DELETE' }); }
...@@ -3,9 +3,9 @@ import { ElMessage } from 'element-plus'; ...@@ -3,9 +3,9 @@ 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: 'company-person', 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'])) }; }
/** 代码作用(白话):提供账号创建、角色编辑、密码重置和逐页权限面板;关联文件:system-user-api-client.js、SystemUserAdminController.java。关联逻辑(调用链/数据流):设置路由 -> 页面操作 -> 管理 API -> 刷新列表。 */ /** 代码作用(白话):提供账号创建、角色编辑、密码重置和逐页权限面板;关联文件:system-user-api-client.js、SystemUserAdminController.java。关联逻辑(调用链/数据流):设置路由 -> 页面操作 -> 管理 API -> 刷新列表。 */
export default { setup() { const users = ref([]); const lockedAccounts = ref([]); const loading = ref(false); const dialogOpen = ref(false); const passwordOpen = ref(false); const editingId = ref(null); const target = ref(null); const form = reactive(blankForm()); const password = ref(''); const isDeveloper = computed(() => authState.user?.roleCode === 'DEVELOPER'); const isAdministratorRole = computed(() => form.roleCode === 'SUPER_ADMIN'); export default { setup() { const users = ref([]); const lockedAccounts = ref([]); const loading = ref(false); const dialogOpen = ref(false); const passwordOpen = ref(false); const editingId = ref(null); const target = ref(null); const form = reactive(blankForm()); const password = ref(''); const isDeveloper = computed(() => authState.user?.roleCode === 'DEVELOPER'); const isAdministratorRole = computed(() => form.roleCode === 'SUPER_ADMIN');
......
...@@ -4,6 +4,7 @@ import PhoneAssetView from '../modules/phone/PhoneAssetView.js'; ...@@ -4,6 +4,7 @@ import PhoneAssetView from '../modules/phone/PhoneAssetView.js';
import WecomAccountView from '../modules/wecom/WecomAccountView.js'; import WecomAccountView from '../modules/wecom/WecomAccountView.js';
import DeviceAssetView from '../modules/device/DeviceAssetView.js'; import DeviceAssetView from '../modules/device/DeviceAssetView.js';
import CompanyProfileView from '../modules/company-profile/CompanyProfileView.js'; import CompanyProfileView from '../modules/company-profile/CompanyProfileView.js';
import CompanyPersonView from '../modules/company-person/CompanyPersonView.js';
import LoginView from '../modules/auth/LoginView.js'; import LoginView from '../modules/auth/LoginView.js';
import UserPermissionView from '../modules/system-user/UserPermissionView.js'; import UserPermissionView from '../modules/system-user/UserPermissionView.js';
import { authState, bootstrapAuth } from '../modules/auth/auth-store.js'; import { authState, bootstrapAuth } from '../modules/auth/auth-store.js';
...@@ -30,6 +31,7 @@ const router = createRouter({ ...@@ -30,6 +31,7 @@ const router = createRouter({
{ path: '/overview', component: createPlaceholderView('资产总览'), meta: { page: 'overview' } }, { path: '/overview', component: createPlaceholderView('资产总览'), meta: { page: 'overview' } },
{ path: '/phone-assets', component: PhoneAssetView, meta: { page: 'phone-assets' } }, { path: '/phone-assets', component: PhoneAssetView, meta: { page: 'phone-assets' } },
{ path: '/company-profiles', component: CompanyProfileView, meta: { page: 'company-profile' } }, { path: '/company-profiles', component: CompanyProfileView, meta: { page: 'company-profile' } },
{ path: '/company-persons', component: CompanyPersonView, meta: { page: 'company-person' } },
{ path: '/device-assets', component: DeviceAssetView, meta: { administratorOnly: true } }, { path: '/device-assets', component: DeviceAssetView, meta: { administratorOnly: true } },
{ path: '/domain', component: createPlaceholderView('域名资料'), meta: { page: 'domain' } }, { path: '/domain', component: createPlaceholderView('域名资料'), meta: { page: 'domain' } },
{ path: '/alerts', component: createPlaceholderView('提醒中心'), meta: { page: 'alerts' } }, { path: '/alerts', component: createPlaceholderView('提醒中心'), meta: { page: 'alerts' } },
......
...@@ -17,6 +17,7 @@ test.beforeEach(async ({ page }) => { ...@@ -17,6 +17,7 @@ test.beforeEach(async ({ page }) => {
'reference-wecom': 'EDIT', 'reference-wecom': 'EDIT',
'phone-assets': 'EDIT', 'phone-assets': 'EDIT',
'company-profile': 'EDIT', 'company-profile': 'EDIT',
'company-person': 'EDIT',
alerts: 'EDIT' alerts: 'EDIT'
} }
}; };
......
import { expect, test } from './authenticated-test.js';
/** 文件用途(白话):验证公司人员菜单、列表、公司搜索、状态联动及增删改主流程。 */
test('manages company people with the reusable asset dialog', async ({ page }) => {
let saved = null; let deletedId = null;
/** 代码作用(白话):模拟人员分页和增删改接口。关联文件:CompanyPersonView.js、company-person-api-client.js。关联逻辑(调用链/数据流):页面请求 -> 测试响应/记录正文 -> UI 断言。 */
await page.route('**/api/company-persons**', async route => {
const url=new URL(route.request().url()); const method=route.request().method();
if(url.pathname.endsWith('/lookups/company-profiles')) { await route.fulfill({contentType:'application/json',body:JSON.stringify({code:200,data:[{id:5,companyName:'示例科技有限公司',shortName:'示例科技'}]})}); return; }
if(method==='POST'||method==='PUT'){saved=route.request().postDataJSON();await route.fulfill({contentType:'application/json',body:JSON.stringify({code:200,data:{id:3,...saved}})});return;}
if(method==='DELETE'){deletedId=url.pathname.split('/').pop();await route.fulfill({contentType:'application/json',body:JSON.stringify({code:200,data:null})});return;}
expect(url.search).toBe('?page=1&size=20');
await route.fulfill({contentType:'application/json',body:JSON.stringify({code:200,data:{records:[{id:3,companyProfileId:5,companyProfileName:'示例科技',personName:'张三',employmentStatus:'在职',resignedAt:null,createTime:'2026-08-12T09:00:00',updateTime:'2026-08-12T09:00:00'}],total:1,page:1,size:20}})});
});
await page.goto('/#/company-persons');
await expect(page.getByRole('link',{name:'公司人员'})).toBeVisible();
await expect(page.getByText('张三')).toBeVisible();
await page.getByRole('button',{name:'新增公司人员'}).click();
await page.getByPlaceholder('请输入人员姓名').fill('李四');
await page.getByRole('dialog',{name:'新增公司人员'}).getByRole('combobox',{name:'所属公司'}).fill('示例');
await page.getByRole('option',{name:'示例科技'}).click();
await page.keyboard.press('Escape');
await page.getByRole('dialog',{name:'新增公司人员'}).getByText('离职',{exact:true}).click();
await page.getByPlaceholder('请选择离职时间').fill('2026-08-12 10:30:00');
await page.getByRole('button',{name:'确认保存'}).click();
await expect.poll(()=>saved?.personName).toBe('李四');
await page.getByRole('button',{name:'删除'}).first().click();
await page.getByRole('button',{name:'确认删除'}).click();
await expect.poll(()=>deletedId).toBe('3');
});
import { expect, test } from './authenticated-test.js'; import { expect, test } from './authenticated-test.js';
/** 文件用途(白话):验证公司档案菜单、展示字段和新增弹窗的主流程,不暴露内部 ID。 */ /** 文件用途(白话):验证公司档案菜单、展示字段和新增弹窗的主流程,不暴露内部 ID。 */
test('shows company profiles and creates one from the reusable asset modal', async ({ page }) => { test('shows and manages company profiles from the reusable asset modal', async ({ page }) => {
let createPayload = null; let createPayload = null;
let updatePayload = null;
let deletedId = null;
let listRequests = 0; let listRequests = 0;
/** /**
...@@ -11,15 +13,17 @@ test('shows company profiles and creates one from the reusable asset modal', asy ...@@ -11,15 +13,17 @@ test('shows company profiles and creates one from the reusable asset modal', asy
* 关联逻辑(调用链/数据流):页面 GET -> 列表数据;弹窗 POST -> 记录请求正文 -> 成功响应 -> 再次 GET。 * 关联逻辑(调用链/数据流):页面 GET -> 列表数据;弹窗 POST -> 记录请求正文 -> 成功响应 -> 再次 GET。
*/ */
await page.route('**/api/company-profiles**', async route => { await page.route('**/api/company-profiles**', async route => {
if (route.request().method() === 'POST') { if (route.request().method() === 'POST' || route.request().method() === 'PUT') {
createPayload = route.request().postDataJSON(); createPayload = route.request().postDataJSON();
if (route.request().method() === 'PUT') updatePayload = createPayload;
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 200, message: '新增成功', data: { companyName: createPayload.companyName } }) }); await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 200, message: '新增成功', data: { companyName: createPayload.companyName } }) });
return; return;
} }
if (route.request().method() === 'DELETE') { deletedId = new URL(route.request().url()).pathname.split('/').pop(); await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 200, data: null }) }); return; }
listRequests += 1; listRequests += 1;
await route.fulfill({ await route.fulfill({
contentType: 'application/json', contentType: 'application/json',
body: JSON.stringify({ code: 200, data: { records: [{ companyName: '示例科技有限公司', shortName: '示例科技', unifiedSocialCreditCode: '91330100TEST000001', address: '杭州市西湖区', contactName: '张三', contactValue: '13812345678', createTime: '2026-08-05T10:00:00', updateTime: '2026-08-05T11:00:00' }], total: 1, page: 1, size: 20 } }) body: JSON.stringify({ code: 200, data: { records: [{ id: 10, companyName: '示例科技有限公司', shortName: '示例科技', unifiedSocialCreditCode: '91330100TEST000001', address: '杭州市西湖区', contactName: '张三', contactValue: '13812345678', createTime: '2026-08-05T10:00:00', updateTime: '2026-08-05T11:00:00' }], total: 1, page: 1, size: 20 } })
}); });
}); });
...@@ -41,4 +45,11 @@ test('shows company profiles and creates one from the reusable asset modal', asy ...@@ -41,4 +45,11 @@ test('shows company profiles and creates one from the reusable asset modal', asy
await expect(page.getByRole('dialog', { name: '新增公司档案' })).toHaveCount(0); await expect(page.getByRole('dialog', { name: '新增公司档案' })).toHaveCount(0);
await expect(page.getByText('新增成功')).toBeVisible(); await expect(page.getByText('新增成功')).toBeVisible();
await expect.poll(() => listRequests).toBeGreaterThan(1); await expect.poll(() => listRequests).toBeGreaterThan(1);
await page.getByRole('button', { name: '编辑' }).first().click();
await page.getByPlaceholder('请输入公司简称').fill('更新简称');
await page.getByRole('button', { name: '保存修改' }).click();
await expect.poll(() => updatePayload?.shortName).toBe('更新简称');
await page.getByRole('button', { name: '删除' }).first().click();
await page.getByRole('button', { name: '确认删除' }).click();
await expect.poll(() => deletedId).toBe('10');
}); });
...@@ -107,6 +107,10 @@ test('确认保存仍调用原有新增接口并提交表单数据', async ({ pa ...@@ -107,6 +107,10 @@ test('确认保存仍调用原有新增接口并提交表单数据', async ({ pa
}); });
const dialog = await openCreateDialog(page); const dialog = await openCreateDialog(page);
await dialog.getByLabel('手机号').fill('13812345678'); await dialog.getByLabel('手机号').fill('13812345678');
await dialog.getByLabel('实名人').fill('张三');
await dialog.locator('.phone-asset-modal__form-row').filter({ hasText: '运营商' }).locator('.el-select').click();
await page.getByRole('option', { name: '中国移动' }).click();
await dialog.getByLabel('ICCID').fill('89860012345678901234');
await dialog.getByRole('button', { name: '确认保存' }).click(); await dialog.getByRole('button', { name: '确认保存' }).click();
await expect.poll(() => submittedPayload?.phoneNumber).toBe('13812345678'); await expect.poll(() => submittedPayload?.phoneNumber).toBe('13812345678');
await expect(dialog).toBeHidden(); await expect(dialog).toBeHidden();
......
...@@ -7,6 +7,11 @@ import { defineConfig } from 'vite'; ...@@ -7,6 +7,11 @@ import { defineConfig } from 'vite';
*/ */
export default defineConfig({ export default defineConfig({
base: '/asset/', base: '/asset/',
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
server: { server: {
port: 5173, port: 5173,
strictPort: true, strictPort: true,
......
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