Commit 36b73908 by DaiJiezhang

feat: add company people management

parent 25564ae2
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;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
......@@ -52,4 +55,19 @@ public class CompanyProfileController {
permissions.require(PagePermissionService.COMPANY_PROFILE, "EDIT");
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;
/** 文件用途(白话):定义公司档案页面允许展示的字段,不暴露内部 ID 和软删除标志。 */
public record CompanyProfileResponse(
Long id,
String companyName,
String shortName,
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;
import com.xyw.console.asset.dto.CompanyProfileResponse;
import com.xyw.console.asset.dto.CompanyProfileSaveRequest;
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.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.util.List;
import org.springframework.stereotype.Service;
......@@ -16,14 +27,26 @@ import org.springframework.stereotype.Service;
@Service
public class CompanyProfileService {
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。
* 关联逻辑(调用链/数据流):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.personMapper = personMapper;
this.wecomMapper = wecomMapper;
this.douyinMapper = douyinMapper;
this.domainMapper = domainMapper;
this.merchantMapper = merchantMapper;
}
/**
......@@ -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。
* 关联逻辑(调用链/数据流):页面关键词 -> 查询条件 -> as_company_profile SQL where/order by。
......@@ -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 和软删除标志。
* 关联文件:CompanyProfileEntity.java、CompanyProfileResponse.java。
* 关联逻辑(调用链/数据流):数据库实体 -> Response DTO -> ApiResponse -> Vue 表格。
*/
private CompanyProfileResponse toResponse(CompanyProfileEntity entity) {
return new CompanyProfileResponse(
entity.getId(),
entity.getCompanyName(),
entity.getShortName(),
entity.getUnifiedSocialCreditCode(),
......
......@@ -17,8 +17,9 @@ public class PagePermissionService {
public static final String WECOM = "reference-wecom";
public static final String PHONE = "phone-assets";
public static final String COMPANY_PROFILE = "company-profile";
public static final String COMPANY_PERSON = "company-person";
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;
/** 代码作用(白话):接收 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;
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.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.status;
......@@ -27,7 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
class CompanyProfileControllerTest {
/**
* 代码作用(白话):验证公司档案 GET 接口返回完整可读字段但不返回内部 ID 或软删除标志。
* 代码作用(白话):验证公司档案 GET 接口返回编辑删除定位所需 ID 和可读字段,但不返回软删除标志。
* 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileResponse.java。
* 关联逻辑(调用链/数据流):GET /api/company-profiles -> Controller -> Service -> JSON 表格数据。
*/
......@@ -47,7 +49,7 @@ class CompanyProfileControllerTest {
.andExpect(jsonPath("$.data.records[0].address", is("杭州市西湖区")))
.andExpect(jsonPath("$.data.records[0].contactName", is("张三")))
.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());
}
......@@ -69,7 +71,7 @@ class CompanyProfileControllerTest {
}
/**
* 代码作用(白话):验证拥有编辑权限时可以通过 POST 新增公司档案,且响应不含内部 ID。
* 代码作用(白话):验证拥有编辑权限时可以通过 POST 新增公司档案,并返回后续编辑可用的 ID。
* 关联文件:CompanyProfileController.java、CompanyProfileService.java、CompanyProfileView.js。
* 关联逻辑(调用链/数据流):新增弹窗 -> POST -> EDIT 权限校验 -> Service.create -> JSON 响应。
*/
......@@ -86,7 +88,7 @@ class CompanyProfileControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code", is(200)))
.andExpect(jsonPath("$.data.companyName", is("示例科技有限公司")))
.andExpect(jsonPath("$.data.id").doesNotExist())
.andExpect(jsonPath("$.data.id", is(10)))
.andExpect(jsonPath("$.data.deleteTime").doesNotExist());
verify(service).create(any());
}
......@@ -110,6 +112,18 @@ class CompanyProfileControllerTest {
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 断言所需的只读公司档案分页结果。
* 关联文件:CompanyProfilePageResponse.java、CompanyProfileController.java。
......@@ -117,7 +131,7 @@ class CompanyProfileControllerTest {
*/
private CompanyProfilePageResponse response() {
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);
}
}
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;
import com.xyw.console.asset.dto.CompanyProfileSaveRequest;
import com.xyw.console.asset.entity.CompanyProfileEntity;
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.util.List;
import org.junit.jupiter.api.Test;
......@@ -34,7 +39,7 @@ class CompanyProfileServiceTest {
page.setTotal(1L);
when(mapper.selectPage(any(), any())).thenReturn(page);
CompanyProfilePageResponse result = new CompanyProfileService(mapper)
CompanyProfilePageResponse result = service(mapper)
.page(new CompanyProfilePageQuery(null, null, null));
CompanyProfileResponse record = result.records().get(0);
......@@ -59,7 +64,7 @@ class CompanyProfileServiceTest {
void createsCompanyProfileWithServerManagedAuditFields() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
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(
" 示例科技有限公司 ", " 示例科技 ", " 91330100TEST000001 ", " 杭州市西湖区 ", " 张三 ", " 13812345678 "));
......@@ -88,13 +93,64 @@ class CompanyProfileServiceTest {
void rejectsBlankCompanyNameBeforeInsert() {
CompanyProfileMapper mapper = mock(CompanyProfileMapper.class);
assertThrows(IllegalArgumentException.class, () -> new CompanyProfileService(mapper)
assertThrows(IllegalArgumentException.class, () -> service(mapper)
.create(new CompanyProfileSaveRequest(" ", null, null, null, null, null)));
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。
* 关联逻辑(调用链/数据流):测试实体 -> Mapper 分页结果 -> Service 转换 -> Response 断言。
......
......@@ -30,5 +30,5 @@ export default {
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 { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
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 与审计字段编辑入口。 */
export default {
......@@ -16,6 +16,7 @@ export default {
const records = ref([]);
const total = ref(0);
const dialogVisible = ref(false);
const editingId = ref(null);
const filters = reactive({ page: 1, size: 20, keyword: '' });
const form = reactive({ companyName: '', shortName: '', unifiedSocialCreditCode: '', address: '', contactName: '', contactValue: '' });
const canEdit = computed(() => authState.user?.pagePermissions?.['company-profile'] === 'EDIT');
......@@ -58,11 +59,15 @@ export default {
* 关联文件:CompanyProfileView.js、app.css。
* 关联逻辑(调用链/数据流):EDIT 用户点击新增 -> resetForm -> dialogVisible=true -> Element Plus 弹窗。
*/
function openCreate() {
function openCreate() {
resetForm();
editingId.value = null;
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。
......@@ -76,8 +81,9 @@ export default {
}
saving.value = true;
try {
await createCompanyProfile({ ...form, companyName });
ElMessage.success('新增成功');
const payload={ ...form, companyName };
if(editingId.value===null) await createCompanyProfile(payload); else await updateCompanyProfile(editingId.value,payload);
ElMessage.success(editingId.value===null?'新增成功':'修改成功');
dialogVisible.value = false;
filters.page = 1;
await loadPage();
......@@ -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。
......@@ -150,14 +159,14 @@ export default {
}
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: `
<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>
<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>
<el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" title="新增公司档案" width="560px">
<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="editingId===null?'新增公司档案':'编辑公司档案'" width="560px">
<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="公司简称"><el-input v-model="form.shortName" maxlength="100" autocomplete="off" placeholder="请输入公司简称" /></el-form-item>
......@@ -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.contactValue" maxlength="100" autocomplete="off" placeholder="请输入联系方式" /></el-form-item>
</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>
</section>
`
......
......@@ -36,3 +36,9 @@ export function listCompanyProfiles(query) {
export function createCompanyProfile(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' }); }
......@@ -291,6 +291,15 @@ export default {
if (value === '已注销') return '注销';
return value || '未设置';
}
/** 代码作用(白话):把存储的运营商值映射成带"中国"前缀的展示文案,历史数据与新选项统一显示。关联文件:PhoneAssetView.js、WecomAccountView.js。关联逻辑(调用链/数据流):列表记录 -> formatCardType -> 表格运营商列。 */
function formatCardType(value) {
if (value === '移动') return '中国移动';
if (value === '电信') return '中国电信';
if (value === '联通') return '中国联通';
if (value === '电销卡「联通」') return '电销卡(中国联通)';
if (value === '广电') return '中国广电';
return value || '未设置';
}
onMounted(() => {
loadPage();
});
......@@ -306,6 +315,7 @@ export default {
editingId,
elementLocale,
filters,
formatCardType,
formatDisposalStatus,
form,
handleIccidPaste,
......@@ -332,8 +342,8 @@ export default {
<el-config-provider :locale="elementLocale"><section class="phone-asset-page phone-asset-list-page">
<header class="phone-asset-list-page__header"><h2>手机号码管理</h2><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" aria-label="筛选手机号码管理"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.phoneNumber" maxlength="11" inputmode="numeric" placeholder="手机号前3位、后4位或完整号码" @input="limitSearchPhone" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.iccid" maxlength="20" placeholder="请输入 ICCID" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.realNameOwner" placeholder="请输入实名人" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-select v-model="filters.disposalStatus" placeholder="使用状态:" clearable @change="changeStatus" @clear="restoreAllDisposalStatuses"><template #prefix>使用状态:</template><el-option label="全部" value="ALL" /><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><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 label="手机号" min-width="180"><template #default="{ row }"><span class="phone-asset-list-page__phone"><span class="phone-asset-list-page__phone-text">{{ row.phoneNumber }}</span><el-popover v-if="row.iccid" placement="right" trigger="click" :width="250" popper-class="phone-asset-list-page__iccid-popper"><template #reference><button type="button" class="phone-asset-list-page__iccid-btn" title="查看 ICCID" aria-label="查看 ICCID"><svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><rect x="2" y="3.2" width="12" height="9.6" rx="1.6" /><path d="M5 6.4h6M5 9.2h3.5" /></svg></button></template><div class="phone-asset-list-page__iccid-pop"><span class="phone-asset-list-page__iccid-pop-label">ICCID</span><span class="phone-asset-list-page__iccid-pop-value">{{ row.iccid }}</span></div></el-popover></span></template></el-table-column><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip /><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column label="关联设备" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.deviceName || '-' }}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120"><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" @opened="resetDialogScroll">
<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 label="手机号" min-width="180"><template #default="{ row }"><span class="phone-asset-list-page__phone"><span class="phone-asset-list-page__phone-text">{{ row.phoneNumber }}</span><el-popover v-if="row.iccid" placement="right" trigger="click" :width="250" popper-class="phone-asset-list-page__iccid-popper"><template #reference><button type="button" class="phone-asset-list-page__iccid-btn" title="查看 ICCID" aria-label="查看 ICCID"><svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><rect x="2" y="3.2" width="12" height="9.6" rx="1.6" /><path d="M5 6.4h6M5 9.2h3.5" /></svg></button></template><div class="phone-asset-list-page__iccid-pop"><span class="phone-asset-list-page__iccid-pop-label">ICCID</span><span class="phone-asset-list-page__iccid-pop-value">{{ row.iccid }}</span></div></el-popover></span></template></el-table-column><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip><template #default="{ row }">{{ formatCardType(row.cardType) }}</template></el-table-column><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column label="关联设备" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.deviceName || '-' }}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120"><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 phone-asset-modal--phone" modal-class="phone-asset-modal-mask" :title="editingId === null ? '新增手机号码管理' : '编辑手机号码管理'" width="560px" @opened="resetDialogScroll">
<el-form class="phone-asset-modal__form" label-width="96px">
<el-form-item class="phone-asset-modal__form-row" label="手机号" required>
<el-input :model-value="form.phoneNumber" class="phone-asset-modal__count-input" maxlength="11" inputmode="numeric" autocomplete="off" placeholder="请输入手机号" @update:model-value="limitPhoneNumber" @paste="handlePhonePaste">
......@@ -342,7 +352,7 @@ export default {
</el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="实名人" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.realNameOwner" maxlength="20" placeholder="请输入实名人" /></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="运营商">
<el-select v-model="form.cardType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="电销卡「联通」" value="电销卡「联通」" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select>
<el-select v-model="form.cardType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="中国移动" value="移动" /><el-option label="中国电信" value="电信" /><el-option label="中国联通" value="联通" /><el-option label="电销卡(中国联通)" value="电销卡「联通」" /><el-option label="中国广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select>
</el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="ICCID" :required="form.cardType !== '虚拟号码'">
<el-input v-model="form.iccid" class="phone-asset-modal__count-input" maxlength="20" autocomplete="off" placeholder="请输入 ICCID" @input="limitIccid" @paste="handleIccidPaste">
......
......@@ -3,9 +3,9 @@ import { ElMessage } from 'element-plus';
import { authState } from '../auth/auth-store.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: '运营' }];
/** 代码作用(白话):创建五页均无权限的编辑表单初始值;关联文件: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'])) }; }
/** 代码作用(白话):提供账号创建、角色编辑、密码重置和逐页权限面板;关联文件: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');
......
......@@ -21,7 +21,7 @@ export default {
const canEdit = computed(() => authState.user?.pagePermissions?.['reference-wecom'] === 'EDIT');
let searchTimer;
const filters = reactive({ page: 1, size: 20, keyword: '', wecomAccount: '', phoneAssetId: new URLSearchParams(window.location.hash.split('?')[1] || '').get('phoneAssetId') || '', companyProfileId: 'ALL', realNameOwnerStatus: 'ALL' });
const form = reactive({ wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' });
const form = reactive({ wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' });
/** 代码作用(白话):注册手机号在手机号码管理中不存在时置为 true,弹窗展开"新增手机号"信息区块。关联文件:wecom-api-client.js、WecomAccountService.java。关联逻辑(调用链/数据流):手机号输入 -> checkPhoneExists -> isNewPhone -> 模板 v-if 区块。 */
const isNewPhone = ref(false);
let phoneCheckTimer = null;
......@@ -34,7 +34,7 @@ export default {
}
/** Code purpose (plain language): resets the create form to the business defaults. Related files: WecomAccountSaveRequest.java. Data flow: add button -> reset -> dialog form. */
function resetForm() { Object.assign(form, { wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' }); isNewPhone.value = false; }
function resetForm() { Object.assign(form, { wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' }); isNewPhone.value = false; }
/**
* 代码作用(白话):在统一资产弹窗打开后把正文滚动位置恢复到顶部,保证标题、表单起点和底部操作区的使用体验一致。
......@@ -58,7 +58,7 @@ export default {
wecomName: row.wecomName || '', wecomAlias: row.wecomAlias || '', wecomAccount: row.wecomAccount || '',
companyProfileId: row.companyProfileId ?? null, phoneNumber: row.phoneNumber || '',
realNameOwner: row.realNameOwner || '', realNameOwnerStatus: row.realNameOwnerStatus || '在职',
gender: row.gender || '', deviceId: row.deviceId ?? null, operatorPersonId: row.operatorPersonId ?? null,
gender: row.gender || '', deviceId: row.deviceId ?? null, operatorPersonId: row.operatorPersonId ?? null,
cardType: '', iccid: '', phoneRealNameOwner: ''
});
isNewPhone.value = false;
......@@ -205,7 +205,7 @@ export default {
<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" /><el-select v-model="filters.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="注册主体:" @change="submitSearch" @clear="restoreAllCompanyProfiles"><template #prefix>注册主体:</template><el-option label="全部" value="ALL" /><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.realNameOwnerStatus" clearable placeholder="实名状态:" @change="submitSearch" @clear="restoreAllRealNameStatuses"><template #prefix>实名状态:</template><el-option label="全部" value="ALL" /><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 wecom-account-page__grid"><el-table-column label="企微名称" min-width="200" show-overflow-tooltip><template #default="{ row }"><span class="wecom-account-page__name"><span class="wecom-account-page__name-main"><span class="wecom-account-page__name-text">{{ row.wecomName }}</span><span v-if="genderClass(row)" class="wecom-account-page__gender" :class="genderClass(row)" :title="'性别:' + row.gender"><svg v-if="row.gender === '男'" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10" cy="14.5" r="5.5"/><path d="M14.5 10L20 4.5M15 4.5h5v5"/></svg><svg v-else viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="9" r="5.5"/><path d="M12 14.5v6M9 18h6"/></svg></span></span><span v-if="row.wecomAlias" class="wecom-account-page__name-alias">{{ row.wecomAlias }}</span></span></template></el-table-column><el-table-column prop="wecomAccount" label="企微账号" min-width="160" show-overflow-tooltip /><el-table-column label="注册主体" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ formatRelation(row.companyProfileName) }}</template></el-table-column><el-table-column label="注册手机号" min-width="170"><template #default="{ row }"><span v-if="row.phoneNumber" class="wecom-account-page__phone"><a class="phone-asset-list-page__external-link" :href="phoneAssetLink(row)" :title="'在手机号码管理中查看 ' + row.phoneNumber">{{ row.phoneNumber }}</a><span class="wecom-account-page__phone-tag" :class="row.phoneLinkMode === 'CREATED' ? 'is-created' : 'is-existing'" :title="row.phoneLinkMode === 'CREATED' ? '新建号码:新增这条企微时自动创建' : '已有号码:关联的是已存在的手机号码管理'">{{ row.phoneLinkMode === 'CREATED' ? '新' : '已' }}</span></span><span v-else>—</span></template></el-table-column><el-table-column label="实名人" min-width="120"><template #default="{ row }"><span v-if="row.realNameOwner" class="wecom-account-page__owner"><span class="wecom-account-page__owner-main">{{ row.realNameOwner }}</span><i v-if="row.realNameOwnerStatus" class="wecom-account-page__owner-dot" :class="{ 'is-left': row.realNameOwnerStatus === '离职' }" :title="'实名状态:' + row.realNameOwnerStatus"></i></span><span v-else>—</span></template></el-table-column><el-table-column label="关联设备" min-width="160" show-overflow-tooltip><template #default="{ row }"><a v-if="deviceAssetLink(row)" class="phone-asset-list-page__external-link" :href="deviceAssetLink(row)" :title="'在设备资产管理中查看 ' + row.deviceName">{{ row.deviceName }}</a><span v-else>—</span></template></el-table-column><el-table-column label="企微号归属人" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ formatRelation(row.operatorPersonName) }}</template></el-table-column><el-table-column label="创建时间" min-width="120"><template #default="{ row }">{{ formatDate(row.createTime) }}</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" @opened="resetDialogScroll"><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.wecomName" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微别名"><el-input v-model="form.wecomAlias" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微账号"><el-input v-model="form.wecomAccount" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="注册手机号" required><el-autocomplete :model-value="form.phoneNumber" :fetch-suggestions="fetchPhoneSuggestions" maxlength="11" inputmode="numeric" placeholder="输入 11 位手机号" style="width:100%" @update:model-value="limitPhoneNumber"><template #suffix><span class="phone-asset-modal__character-count">{{ form.phoneNumber.length }}/11</span></template></el-autocomplete></el-form-item><template v-if="isNewPhone"><div class="phone-asset-modal__form-divider">新增手机号</div><el-form-item class="phone-asset-modal__form-row" label="运营商"><el-select v-model="form.cardType" clearable placeholder="请选择" style="width:100%"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="电销卡「联通」" value="电销卡「联通」" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="ICCID" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.iccid" maxlength="20" placeholder="请输入 ICCID" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="手机号实名人" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.phoneRealNameOwner" maxlength="20" placeholder="请输入手机号实名人" /></el-form-item></template><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="关联设备"><el-select v-model="form.deviceId" filterable remote clearable :remote-method="loadDevices" placeholder="输入设备名称搜索" style="width:100%"><el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微号归属人"><el-select v-model="form.operatorPersonId" filterable remote clearable :remote-method="loadOwners" placeholder="输入人员姓名" style="width:100%"><el-option v-for="item in ownerOptions" :key="item.id" :label="item.personName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名状态"><el-radio-group v-model="form.realNameOwnerStatus"><el-radio value="在职">在职</el-radio><el-radio value="离职">离职</el-radio></el-radio-group></el-form-item><el-form-item class="phone-asset-modal__form-row" label="性别"><el-radio-group v-model="form.gender"><el-radio value="男">男</el-radio><el-radio value="女">女</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">保存</el-button></template></el-dialog>
<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" @opened="resetDialogScroll"><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.wecomName" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微别名"><el-input v-model="form.wecomAlias" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微账号"><el-input v-model="form.wecomAccount" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="注册手机号" required><el-autocomplete :model-value="form.phoneNumber" :fetch-suggestions="fetchPhoneSuggestions" maxlength="11" inputmode="numeric" placeholder="输入 11 位手机号" style="width:100%" @update:model-value="limitPhoneNumber"><template #suffix><span class="phone-asset-modal__character-count">{{ form.phoneNumber.length }}/11</span></template></el-autocomplete></el-form-item><template v-if="isNewPhone"><div class="phone-asset-modal__form-divider">新增手机号</div><el-form-item class="phone-asset-modal__form-row" label="运营商"><el-select v-model="form.cardType" clearable placeholder="请选择" style="width:100%"><el-option label="中国移动" value="移动" /><el-option label="中国电信" value="电信" /><el-option label="中国联通" value="联通" /><el-option label="电销卡(中国联通)" value="电销卡「联通」" /><el-option label="中国广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="ICCID" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.iccid" maxlength="20" placeholder="请输入 ICCID" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="手机号实名人" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.phoneRealNameOwner" maxlength="20" placeholder="请输入手机号实名人" /></el-form-item></template><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="关联设备"><el-select v-model="form.deviceId" filterable remote clearable :remote-method="loadDevices" placeholder="输入设备名称搜索" style="width:100%"><el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微号归属人"><el-select v-model="form.operatorPersonId" filterable remote clearable :remote-method="loadOwners" placeholder="输入人员姓名" style="width:100%"><el-option v-for="item in ownerOptions" :key="item.id" :label="item.personName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名状态"><el-radio-group v-model="form.realNameOwnerStatus"><el-radio value="在职">在职</el-radio><el-radio value="离职">离职</el-radio></el-radio-group></el-form-item><el-form-item class="phone-asset-modal__form-row" label="性别"><el-radio-group v-model="form.gender"><el-radio value="男">男</el-radio><el-radio value="女">女</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">保存</el-button></template></el-dialog>
</section>
`
};
......@@ -4,6 +4,7 @@ import PhoneAssetView from '../modules/phone/PhoneAssetView.js';
import WecomAccountView from '../modules/wecom/WecomAccountView.js';
import DeviceAssetView from '../modules/device/DeviceAssetView.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 UserPermissionView from '../modules/system-user/UserPermissionView.js';
import { authState, bootstrapAuth } from '../modules/auth/auth-store.js';
......@@ -30,6 +31,7 @@ const router = createRouter({
{ path: '/overview', component: createPlaceholderView('资产总览'), meta: { page: 'overview' } },
{ path: '/phone-assets', component: PhoneAssetView, meta: { page: 'phone-assets' } },
{ 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: '/domain', component: createPlaceholderView('域名资料'), meta: { page: 'domain' } },
{ path: '/alerts', component: createPlaceholderView('提醒中心'), meta: { page: 'alerts' } },
......
......@@ -61,6 +61,11 @@ body { margin: 0; }
background: #ffffff;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.14);
}
/* 手机号码管理弹窗:高度贴合内容,内容多时才限高滚动(企微等其余弹窗保持固定高度)。 */
.phone-asset-modal--phone {
height: auto;
max-height: min(680px, calc(100vh - 48px));
}
.phone-asset-modal .el-dialog__header {
display: flex;
align-items: flex-start;
......
......@@ -17,6 +17,7 @@ test.beforeEach(async ({ page }) => {
'reference-wecom': 'EDIT',
'phone-assets': 'EDIT',
'company-profile': 'EDIT',
'company-person': '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';
/** 文件用途(白话):验证公司档案菜单、展示字段和新增弹窗的主流程,不暴露内部 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 updatePayload = null;
let deletedId = null;
let listRequests = 0;
/**
......@@ -11,15 +13,17 @@ test('shows company profiles and creates one from the reusable asset modal', asy
* 关联逻辑(调用链/数据流):页面 GET -> 列表数据;弹窗 POST -> 记录请求正文 -> 成功响应 -> 再次 GET。
*/
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();
if (route.request().method() === 'PUT') updatePayload = createPayload;
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 200, message: '新增成功', data: { companyName: createPayload.companyName } }) });
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;
await route.fulfill({
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
await expect(page.getByRole('dialog', { name: '新增公司档案' })).toHaveCount(0);
await expect(page.getByText('新增成功')).toBeVisible();
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
});
const dialog = await openCreateDialog(page);
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 expect.poll(() => submittedPayload?.phoneNumber).toBe('13812345678');
await expect(dialog).toBeHidden();
......
......@@ -7,6 +7,11 @@ import { defineConfig } from 'vite';
*/
export default defineConfig({
base: '/asset/',
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
server: {
port: 5173,
strictPort: true,
......@@ -18,4 +23,4 @@ export default defineConfig({
}
},
preview: { port: 4173, strictPort: true }
});
\ No newline at end of file
});
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