Commit 90002afb by DaiJiezhang

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

feat: add company people management

See merge request !14
parents 9ae57966 36b73908
package com.xyw.console.asset.controller;
import com.xyw.console.common.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** 文件用途(白话):把公司档案和公司人员的字段、引用与不存在错误转换成页面可读 JSON。 */
@RestControllerAdvice(assignableTypes={CompanyProfileController.class,CompanyPersonController.class})
public class CompanyManagementExceptionHandler {
/** 代码作用(白话):把保存校验、记录不存在和引用保护提示返回为 400。关联文件:CompanyProfileService.java、CompanyPersonService.java。关联逻辑(调用链/数据流):Service 参数错误 -> Advice -> ApiResponse -> 页面提示。 */
@ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> invalid(IllegalArgumentException error){return ApiResponse.error(400,error.getMessage());}
}
package com.xyw.console.asset.controller;
import com.xyw.console.asset.dto.CompanyPersonPageQuery;
import com.xyw.console.asset.dto.CompanyPersonPageResponse;
import com.xyw.console.asset.dto.CompanyPersonResponse;
import com.xyw.console.asset.dto.CompanyPersonSaveRequest;
import com.xyw.console.asset.dto.CompanyProfileLookupResponse;
import com.xyw.console.asset.service.CompanyPersonService;
import com.xyw.console.auth.PagePermissionService;
import com.xyw.console.common.ApiResponse;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 文件用途(白话):提供公司人员分页、公司搜索和增删改接口,并统一检查独立页面权限。 */
@RestController
@RequestMapping("/api/company-persons")
public class CompanyPersonController {
private final CompanyPersonService service; private final PagePermissionService permissions;
/** 代码作用(白话):接收人员服务和权限服务。关联文件:CompanyPersonService.java、PagePermissionService.java。关联逻辑(调用链/数据流):HTTP -> Controller -> 权限/Service。 */
public CompanyPersonController(CompanyPersonService service,PagePermissionService permissions){this.service=service;this.permissions=permissions;}
/** 代码作用(白话):读取有权限用户可见的公司人员分页。关联文件:CompanyPersonPageQuery.java、CompanyPersonView.js。关联逻辑(调用链/数据流):GET -> READ -> Service.page -> JSON。 */
@GetMapping public ApiResponse<CompanyPersonPageResponse> page(@Valid CompanyPersonPageQuery query){permissions.require(PagePermissionService.COMPANY_PERSON,"READ");return ApiResponse.success(service.page(query));}
/** 代码作用(白话):为人员表单搜索有效所属公司。关联文件:CompanyPersonService.java、CompanyPersonView.js。关联逻辑(调用链/数据流):关键词 -> READ -> 公司选项。 */
@GetMapping("/lookups/company-profiles") public ApiResponse<List<CompanyProfileLookupResponse>> companies(@RequestParam(defaultValue="")String keyword){permissions.require(PagePermissionService.COMPANY_PERSON,"READ");return ApiResponse.success(service.searchCompanies(keyword));}
/** 代码作用(白话):新增公司人员。关联文件:CompanyPersonSaveRequest.java、CompanyPersonService.java。关联逻辑(调用链/数据流):POST 表单 -> EDIT -> create -> JSON。 */
@PostMapping public ApiResponse<CompanyPersonResponse> create(@Valid @RequestBody CompanyPersonSaveRequest request){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");return ApiResponse.success("新增成功",service.create(request));}
/** 代码作用(白话):编辑指定公司人员。关联文件:CompanyPersonSaveRequest.java、CompanyPersonService.java。关联逻辑(调用链/数据流):PUT /{id} -> EDIT -> update -> JSON。 */
@PutMapping("/{id}") public ApiResponse<CompanyPersonResponse> update(@PathVariable Long id,@Valid @RequestBody CompanyPersonSaveRequest request){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");return ApiResponse.success("修改成功",service.update(id,request));}
/** 代码作用(白话):引用检查通过后软删除公司人员。关联文件:CompanyPersonService.java、CompanyPersonView.js。关联逻辑(调用链/数据流):DELETE /{id} -> EDIT -> softDelete -> JSON。 */
@DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.COMPANY_PERSON,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);}
}
......@@ -10,6 +10,9 @@ import com.xyw.console.common.ApiResponse;
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,
......
......@@ -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 { 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'});}
......@@ -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' }); }
......@@ -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');
......
......@@ -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' } },
......
......@@ -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,
......
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