Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xyw_asset_console
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DaiJiezhang
xyw_asset_console
Commits
36b73908
Commit
36b73908
authored
Aug 14, 2026
by
DaiJiezhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add company people management
parent
25564ae2
Hide whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
683 additions
and
34 deletions
+683
-34
CompanyManagementExceptionHandler.java
...e/asset/controller/CompanyManagementExceptionHandler.java
+15
-0
CompanyPersonController.java
...xyw/console/asset/controller/CompanyPersonController.java
+40
-0
CompanyProfileController.java
...yw/console/asset/controller/CompanyProfileController.java
+18
-0
CompanyPersonPageQuery.java
...ava/com/xyw/console/asset/dto/CompanyPersonPageQuery.java
+17
-0
CompanyPersonPageResponse.java
.../com/xyw/console/asset/dto/CompanyPersonPageResponse.java
+7
-0
CompanyPersonResponse.java
...java/com/xyw/console/asset/dto/CompanyPersonResponse.java
+15
-0
CompanyPersonSaveRequest.java
...a/com/xyw/console/asset/dto/CompanyPersonSaveRequest.java
+12
-0
CompanyProfileResponse.java
...ava/com/xyw/console/asset/dto/CompanyProfileResponse.java
+1
-0
CompanyPersonService.java
...a/com/xyw/console/asset/service/CompanyPersonService.java
+84
-0
CompanyProfileService.java
.../com/xyw/console/asset/service/CompanyProfileService.java
+84
-1
PagePermissionService.java
...main/java/com/xyw/console/auth/PagePermissionService.java
+2
-1
CompanyPersonControllerTest.java
...console/asset/controller/CompanyPersonControllerTest.java
+25
-0
CompanyProfileControllerTest.java
...onsole/asset/controller/CompanyProfileControllerTest.java
+19
-5
CompanyPersonServiceTest.java
...m/xyw/console/asset/service/CompanyPersonServiceTest.java
+137
-0
CompanyProfileServiceTest.java
.../xyw/console/asset/service/CompanyProfileServiceTest.java
+59
-3
App.js
frontend/src/App.js
+1
-1
CompanyPersonView.js
frontend/src/modules/company-person/CompanyPersonView.js
+27
-0
company-person-api-client.js
...d/src/modules/company-person/company-person-api-client.js
+15
-0
CompanyProfileView.js
frontend/src/modules/company-profile/CompanyProfileView.js
+18
-9
company-profile-api-client.js
...src/modules/company-profile/company-profile-api-client.js
+6
-0
PhoneAssetView.js
frontend/src/modules/phone/PhoneAssetView.js
+13
-3
UserPermissionView.js
frontend/src/modules/system-user/UserPermissionView.js
+2
-2
WecomAccountView.js
frontend/src/modules/wecom/WecomAccountView.js
+4
-4
index.js
frontend/src/router/index.js
+2
-0
app.css
frontend/src/styles/app.css
+5
-0
authenticated-test.js
frontend/tests/authenticated-test.js
+1
-0
company-person.spec.js
frontend/tests/company-person.spec.js
+30
-0
company-profile.spec.js
frontend/tests/company-profile.spec.js
+14
-3
phone-asset.spec.js
frontend/tests/phone-asset.spec.js
+4
-0
vite.config.js
frontend/vite.config.js
+6
-2
No files found.
backend/src/main/java/com/xyw/console/asset/controller/CompanyManagementExceptionHandler.java
0 → 100644
View file @
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
());}
}
backend/src/main/java/com/xyw/console/asset/controller/CompanyPersonController.java
0 → 100644
View file @
36b73908
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
);}
}
backend/src/main/java/com/xyw/console/asset/controller/CompanyProfileController.java
View file @
36b73908
...
...
@@ -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
);
}
}
backend/src/main/java/com/xyw/console/asset/dto/CompanyPersonPageQuery.java
0 → 100644
View file @
36b73908
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
;
}
}
backend/src/main/java/com/xyw/console/asset/dto/CompanyPersonPageResponse.java
0 → 100644
View file @
36b73908
package
com
.
xyw
.
console
.
asset
.
dto
;
import
java.util.List
;
/** 文件用途(白话):把公司人员记录、总数和分页位置一起返回给列表页面。 */
public
record
CompanyPersonPageResponse
(
List
<
CompanyPersonResponse
>
records
,
long
total
,
int
page
,
int
size
)
{
}
backend/src/main/java/com/xyw/console/asset/dto/CompanyPersonResponse.java
0 → 100644
View file @
36b73908
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
)
{
}
backend/src/main/java/com/xyw/console/asset/dto/CompanyPersonSaveRequest.java
0 → 100644
View file @
36b73908
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
)
{
}
backend/src/main/java/com/xyw/console/asset/dto/CompanyProfileResponse.java
View file @
36b73908
...
...
@@ -4,6 +4,7 @@ import java.time.LocalDateTime;
/** 文件用途(白话):定义公司档案页面允许展示的字段,不暴露内部 ID 和软删除标志。 */
public
record
CompanyProfileResponse
(
Long
id
,
String
companyName
,
String
shortName
,
String
unifiedSocialCreditCode
,
...
...
backend/src/main/java/com/xyw/console/asset/service/CompanyPersonService.java
0 → 100644
View file @
36b73908
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
();
}
}
backend/src/main/java/com/xyw/console/asset/service/CompanyProfileService.java
View file @
36b73908
...
...
@@ -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
(),
...
...
backend/src/main/java/com/xyw/console/auth/PagePermissionService.java
View file @
36b73908
...
...
@@ -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 判定。 */
...
...
backend/src/test/java/com/xyw/console/asset/controller/CompanyPersonControllerTest.java
0 → 100644
View file @
36b73908
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"
);
}
}
backend/src/test/java/com/xyw/console/asset/controller/CompanyProfileControllerTest.java
View file @
36b73908
...
...
@@ -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
);
}
}
backend/src/test/java/com/xyw/console/asset/service/CompanyPersonServiceTest.java
0 → 100644
View file @
36b73908
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
));
}
}
backend/src/test/java/com/xyw/console/asset/service/CompanyProfileServiceTest.java
View file @
36b73908
...
...
@@ -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
CompanyProfileS
ervice
(
mapper
)
CompanyProfilePageResponse
result
=
s
ervice
(
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
CompanyProfileS
ervice
(
mapper
);
CompanyProfileService
service
=
s
ervice
(
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
CompanyProfileS
ervice
(
mapper
)
assertThrows
(
IllegalArgumentException
.
class
,
()
->
s
ervice
(
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 断言。
...
...
frontend/src/App.js
View file @
36b73908
...
...
@@ -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>`
};
frontend/src/modules/company-person/CompanyPersonView.js
0 → 100644
View file @
36b73908
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>`
};
frontend/src/modules/company-person/company-person-api-client.js
0 → 100644
View file @
36b73908
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'
});}
frontend/src/modules/company-profile/CompanyProfileView.js
View file @
36b73908
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>
`
...
...
frontend/src/modules/company-profile/company-profile-api-client.js
View file @
36b73908
...
...
@@ -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'
});
}
frontend/src/modules/phone/PhoneAssetView.js
View file @
36b73908
...
...
@@ -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">
...
...
frontend/src/modules/system-user/UserPermissionView.js
View file @
36b73908
...
...
@@ -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'
);
...
...
frontend/src/modules/wecom/WecomAccountView.js
View file @
36b73908
...
...
@@ -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>
`
};
frontend/src/router/index.js
View file @
36b73908
...
...
@@ -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'
}
},
...
...
frontend/src/styles/app.css
View file @
36b73908
...
...
@@ -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
;
...
...
frontend/tests/authenticated-test.js
View file @
36b73908
...
...
@@ -17,6 +17,7 @@ test.beforeEach(async ({ page }) => {
'reference-wecom'
:
'EDIT'
,
'phone-assets'
:
'EDIT'
,
'company-profile'
:
'EDIT'
,
'company-person'
:
'EDIT'
,
alerts
:
'EDIT'
}
};
...
...
frontend/tests/company-person.spec.js
0 → 100644
View file @
36b73908
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'
);
});
frontend/tests/company-profile.spec.js
View file @
36b73908
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'
);
});
frontend/tests/phone-asset.spec.js
View file @
36b73908
...
...
@@ -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
();
...
...
frontend/vite.config.js
View file @
36b73908
...
...
@@ -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
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment