parent
c6b695169e
commit
aa632ec6ec
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -34,12 +34,14 @@ public class OpportunitySchemaInitializer implements ApplicationRunner {
|
||||||
statement.execute("alter table crm_opportunity add column if not exists pre_sales_name varchar(100)");
|
statement.execute("alter table crm_opportunity add column if not exists pre_sales_name varchar(100)");
|
||||||
statement.execute("alter table crm_opportunity add column if not exists latest_progress text");
|
statement.execute("alter table crm_opportunity add column if not exists latest_progress text");
|
||||||
statement.execute("alter table crm_opportunity add column if not exists next_plan text");
|
statement.execute("alter table crm_opportunity add column if not exists next_plan text");
|
||||||
|
statement.execute("alter table crm_opportunity add column if not exists updated_by bigint");
|
||||||
statement.execute("alter table crm_opportunity add column if not exists archived_at timestamptz");
|
statement.execute("alter table crm_opportunity add column if not exists archived_at timestamptz");
|
||||||
statement.execute("alter table crm_opportunity add column if not exists actual_signed_amount numeric(18, 2)");
|
statement.execute("alter table crm_opportunity add column if not exists actual_signed_amount numeric(18, 2)");
|
||||||
statement.execute("alter table crm_opportunity add column if not exists is_poc boolean not null default false");
|
statement.execute("alter table crm_opportunity add column if not exists is_poc boolean not null default false");
|
||||||
statement.execute("create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at)");
|
statement.execute("create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at)");
|
||||||
statement.execute("comment on column crm_opportunity.latest_progress is '项目最新进展'");
|
statement.execute("comment on column crm_opportunity.latest_progress is '项目最新进展'");
|
||||||
statement.execute("comment on column crm_opportunity.next_plan is '下一步销售计划'");
|
statement.execute("comment on column crm_opportunity.next_plan is '下一步销售计划'");
|
||||||
|
statement.execute("comment on column crm_opportunity.updated_by is '更新人ID'");
|
||||||
statement.execute("comment on column crm_opportunity.archived_at is '归档时间'");
|
statement.execute("comment on column crm_opportunity.archived_at is '归档时间'");
|
||||||
statement.execute("comment on column crm_opportunity.actual_signed_amount is '实际签约金额'");
|
statement.execute("comment on column crm_opportunity.actual_signed_amount is '实际签约金额'");
|
||||||
statement.execute("comment on column crm_opportunity.is_poc is '是否POC测试项目'");
|
statement.execute("comment on column crm_opportunity.is_poc is '是否POC测试项目'");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.unis.crm.common;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserDataScopeSchemaInitializer implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(UserDataScopeSchemaInitializer.class);
|
||||||
|
|
||||||
|
private final DataSource dataSource;
|
||||||
|
|
||||||
|
public UserDataScopeSchemaInitializer(DataSource dataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
try (Connection connection = dataSource.getConnection();
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("""
|
||||||
|
create table if not exists sys_user_data_scope_user (
|
||||||
|
id bigserial primary key,
|
||||||
|
tenant_id bigint not null,
|
||||||
|
viewer_user_id bigint not null,
|
||||||
|
owner_user_id bigint not null,
|
||||||
|
resource_type varchar(50) not null default 'ALL',
|
||||||
|
enabled boolean not null default true,
|
||||||
|
expire_at timestamptz,
|
||||||
|
remark varchar(500),
|
||||||
|
created_by bigint,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
is_deleted integer not null default 0
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
create unique index if not exists uk_user_data_scope_user
|
||||||
|
on sys_user_data_scope_user (tenant_id, viewer_user_id, owner_user_id, resource_type)
|
||||||
|
where is_deleted = 0
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
create index if not exists idx_user_data_scope_viewer
|
||||||
|
on sys_user_data_scope_user (tenant_id, viewer_user_id, resource_type, enabled)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
create index if not exists idx_user_data_scope_owner
|
||||||
|
on sys_user_data_scope_user (tenant_id, owner_user_id)
|
||||||
|
""");
|
||||||
|
statement.execute("comment on table sys_user_data_scope_user is '用户到用户的数据可见范围授权'");
|
||||||
|
statement.execute("comment on column sys_user_data_scope_user.viewer_user_id is '被授权查看数据的用户ID'");
|
||||||
|
statement.execute("comment on column sys_user_data_scope_user.owner_user_id is '数据归属用户ID'");
|
||||||
|
statement.execute("comment on column sys_user_data_scope_user.resource_type is '授权资源类型:OPPORTUNITY/EXPANSION/DAILY_REPORT/CHECKIN等'");
|
||||||
|
log.info("Ensured sys_user_data_scope_user exists");
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
throw new IllegalStateException("Failed to initialize user data scope schema", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.unis.crm.controller;
|
||||||
|
|
||||||
|
import com.unis.crm.common.ApiResponse;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeAssignmentDTO;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeGrantDTO;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeUserDTO;
|
||||||
|
import com.unis.crm.service.UserDataScopeAdminService;
|
||||||
|
import com.unisbase.common.annotation.Log;
|
||||||
|
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.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("/sys/api/admin/user-data-scope")
|
||||||
|
public class UserDataScopeAdminController {
|
||||||
|
|
||||||
|
private final UserDataScopeAdminService userDataScopeAdminService;
|
||||||
|
|
||||||
|
public UserDataScopeAdminController(UserDataScopeAdminService userDataScopeAdminService) {
|
||||||
|
this.userDataScopeAdminService = userDataScopeAdminService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users")
|
||||||
|
public ApiResponse<List<UserDataScopeUserDTO>> listUsers(
|
||||||
|
@RequestParam(value = "tenantId", required = false) Long tenantId) {
|
||||||
|
return ApiResponse.success(userDataScopeAdminService.listUsers(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/grants")
|
||||||
|
public ApiResponse<List<UserDataScopeGrantDTO>> listGrants(
|
||||||
|
@RequestParam(value = "tenantId", required = false) Long tenantId,
|
||||||
|
@RequestParam(value = "viewerUserId", required = false) Long viewerUserId,
|
||||||
|
@RequestParam(value = "resourceType", required = false) String resourceType) {
|
||||||
|
return ApiResponse.success(userDataScopeAdminService.listGrants(tenantId, viewerUserId, resourceType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/assignment")
|
||||||
|
public ApiResponse<UserDataScopeAssignmentDTO> getAssignment(
|
||||||
|
@RequestParam(value = "tenantId", required = false) Long tenantId,
|
||||||
|
@RequestParam("viewerUserId") Long viewerUserId,
|
||||||
|
@RequestParam("resourceType") String resourceType) {
|
||||||
|
return ApiResponse.success(userDataScopeAdminService.getAssignment(tenantId, viewerUserId, resourceType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/assignment")
|
||||||
|
@Log(type = "系统管理", value = "保存用户数据授权")
|
||||||
|
public ApiResponse<Boolean> saveAssignment(@RequestBody UserDataScopeAssignmentDTO payload) {
|
||||||
|
return ApiResponse.success(userDataScopeAdminService.saveAssignment(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/grants/{id}")
|
||||||
|
@Log(type = "系统管理", value = "删除用户数据授权")
|
||||||
|
public ApiResponse<Boolean> deleteGrant(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestParam(value = "tenantId", required = false) Long tenantId) {
|
||||||
|
return ApiResponse.success(userDataScopeAdminService.deleteGrant(tenantId, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.unis.crm.dto.datascope;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserDataScopeAssignmentDTO {
|
||||||
|
|
||||||
|
private Long tenantId;
|
||||||
|
private Long viewerUserId;
|
||||||
|
private String resourceType;
|
||||||
|
private List<String> resourceTypes = new ArrayList<>();
|
||||||
|
private List<Long> ownerUserIds = new ArrayList<>();
|
||||||
|
private OffsetDateTime expireAt;
|
||||||
|
private String remark;
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getViewerUserId() {
|
||||||
|
return viewerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewerUserId(Long viewerUserId) {
|
||||||
|
this.viewerUserId = viewerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceType() {
|
||||||
|
return resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResourceType(String resourceType) {
|
||||||
|
this.resourceType = resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getResourceTypes() {
|
||||||
|
return resourceTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResourceTypes(List<String> resourceTypes) {
|
||||||
|
this.resourceTypes = resourceTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getOwnerUserIds() {
|
||||||
|
return ownerUserIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwnerUserIds(List<Long> ownerUserIds) {
|
||||||
|
this.ownerUserIds = ownerUserIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getExpireAt() {
|
||||||
|
return expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpireAt(OffsetDateTime expireAt) {
|
||||||
|
this.expireAt = expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package com.unis.crm.dto.datascope;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
public class UserDataScopeGrantDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long viewerUserId;
|
||||||
|
private String viewerName;
|
||||||
|
private Long ownerUserId;
|
||||||
|
private String ownerName;
|
||||||
|
private String resourceType;
|
||||||
|
private Boolean enabled;
|
||||||
|
private OffsetDateTime expireAt;
|
||||||
|
private String remark;
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getViewerUserId() {
|
||||||
|
return viewerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewerUserId(Long viewerUserId) {
|
||||||
|
this.viewerUserId = viewerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getViewerName() {
|
||||||
|
return viewerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewerName(String viewerName) {
|
||||||
|
this.viewerName = viewerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOwnerUserId() {
|
||||||
|
return ownerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwnerUserId(Long ownerUserId) {
|
||||||
|
this.ownerUserId = ownerUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwnerName() {
|
||||||
|
return ownerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwnerName(String ownerName) {
|
||||||
|
this.ownerName = ownerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceType() {
|
||||||
|
return resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResourceType(String resourceType) {
|
||||||
|
this.resourceType = resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getExpireAt() {
|
||||||
|
return expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpireAt(OffsetDateTime expireAt) {
|
||||||
|
this.expireAt = expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.unis.crm.dto.datascope;
|
||||||
|
|
||||||
|
public class UserDataScopeUserDTO {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
private String username;
|
||||||
|
private String displayName;
|
||||||
|
private String orgName;
|
||||||
|
private Integer status;
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrgName() {
|
||||||
|
return orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrgName(String orgName) {
|
||||||
|
this.orgName = orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(Integer status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ public class OpportunityFollowUpDTO {
|
||||||
private String communicationContent;
|
private String communicationContent;
|
||||||
private String nextAction;
|
private String nextAction;
|
||||||
private String user;
|
private String user;
|
||||||
|
private String roleCodes;
|
||||||
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
|
|
@ -98,6 +99,14 @@ public class OpportunityFollowUpDTO {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRoleCodes() {
|
||||||
|
return roleCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoleCodes(String roleCodes) {
|
||||||
|
this.roleCodes = roleCodes;
|
||||||
|
}
|
||||||
|
|
||||||
public List<WorkReportAttachmentDTO> getAttachments() {
|
public List<WorkReportAttachmentDTO> getAttachments() {
|
||||||
return attachments;
|
return attachments;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.unis.crm.dto.work;
|
package com.unis.crm.dto.work;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class WorkCheckInExportDTO {
|
public class WorkCheckInExportDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
private String checkinDate;
|
private String checkinDate;
|
||||||
private String checkinTime;
|
private String checkinTime;
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
@ -19,8 +21,17 @@ public class WorkCheckInExportDTO {
|
||||||
private String status;
|
private String status;
|
||||||
private String createdAt;
|
private String createdAt;
|
||||||
private String updatedAt;
|
private String updatedAt;
|
||||||
|
private OffsetDateTime sortTime;
|
||||||
private List<String> photoUrls = new ArrayList<>();
|
private List<String> photoUrls = new ArrayList<>();
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCheckinDate() {
|
public String getCheckinDate() {
|
||||||
return checkinDate;
|
return checkinDate;
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +136,14 @@ public class WorkCheckInExportDTO {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getSortTime() {
|
||||||
|
return sortTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortTime(OffsetDateTime sortTime) {
|
||||||
|
this.sortTime = sortTime;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getPhotoUrls() {
|
public List<String> getPhotoUrls() {
|
||||||
return photoUrls;
|
return photoUrls;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ package com.unis.crm.dto.work;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
public class WorkDailyReportExportDTO {
|
public class WorkDailyReportExportDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
private String reportDate;
|
private String reportDate;
|
||||||
private String submitTime;
|
private String submitTime;
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
@ -19,10 +21,19 @@ public class WorkDailyReportExportDTO {
|
||||||
private String reviewedAt;
|
private String reviewedAt;
|
||||||
private String createdAt;
|
private String createdAt;
|
||||||
private String updatedAt;
|
private String updatedAt;
|
||||||
|
private OffsetDateTime sortTime;
|
||||||
private List<WorkReportLineItemDTO> lineItems = new ArrayList<>();
|
private List<WorkReportLineItemDTO> lineItems = new ArrayList<>();
|
||||||
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
||||||
private List<WorkTomorrowPlanItemDTO> planItems = new ArrayList<>();
|
private List<WorkTomorrowPlanItemDTO> planItems = new ArrayList<>();
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
public String getReportDate() {
|
public String getReportDate() {
|
||||||
return reportDate;
|
return reportDate;
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +146,14 @@ public class WorkDailyReportExportDTO {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getSortTime() {
|
||||||
|
return sortTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortTime(OffsetDateTime sortTime) {
|
||||||
|
this.sortTime = sortTime;
|
||||||
|
}
|
||||||
|
|
||||||
public List<WorkReportLineItemDTO> getLineItems() {
|
public List<WorkReportLineItemDTO> getLineItems() {
|
||||||
return lineItems;
|
return lineItems;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,17 @@ public interface ExpansionMapper {
|
||||||
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
|
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
|
||||||
List<SalesExpansionItemDTO> selectSalesExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
|
List<SalesExpansionItemDTO> selectSalesExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
|
||||||
|
|
||||||
|
List<SalesExpansionItemDTO> selectSalesExpansionsByOwnerUserIds(
|
||||||
|
@Param("ownerUserIds") List<Long> ownerUserIds,
|
||||||
|
@Param("keyword") String keyword);
|
||||||
|
|
||||||
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
|
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
|
||||||
List<ChannelExpansionItemDTO> selectChannelExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
|
List<ChannelExpansionItemDTO> selectChannelExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
|
||||||
|
|
||||||
|
List<ChannelExpansionItemDTO> selectChannelExpansionsByOwnerUserIds(
|
||||||
|
@Param("ownerUserIds") List<Long> ownerUserIds,
|
||||||
|
@Param("keyword") String keyword);
|
||||||
|
|
||||||
List<SalesExpansionItemDTO> selectSalesExpansionsForTenant(
|
List<SalesExpansionItemDTO> selectSalesExpansionsForTenant(
|
||||||
@Param("tenantId") Long tenantId,
|
@Param("tenantId") Long tenantId,
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
|
|
@ -45,19 +53,14 @@ public interface ExpansionMapper {
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("limit") Integer limit);
|
@Param("limit") Integer limit);
|
||||||
|
|
||||||
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
|
|
||||||
List<ExpansionFollowUpDTO> selectSalesFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
List<ExpansionFollowUpDTO> selectSalesFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||||
|
|
||||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
|
||||||
List<RelatedProjectSummaryDTO> selectSalesRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
List<RelatedProjectSummaryDTO> selectSalesRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||||
|
|
||||||
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
|
|
||||||
List<ExpansionFollowUpDTO> selectChannelFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
List<ExpansionFollowUpDTO> selectChannelFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||||
|
|
||||||
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
|
|
||||||
List<ChannelExpansionContactDTO> selectChannelContacts(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
List<ChannelExpansionContactDTO> selectChannelContacts(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||||
|
|
||||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
|
||||||
List<ChannelRelatedProjectSummaryDTO> selectChannelRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
List<ChannelRelatedProjectSummaryDTO> selectChannelRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||||
|
|
||||||
int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request);
|
int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request);
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,23 @@ public interface WorkMapper {
|
||||||
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
||||||
List<WorkHistoryItemDTO> selectCheckInHistory(@Param("limit") int limit);
|
List<WorkHistoryItemDTO> selectCheckInHistory(@Param("limit") int limit);
|
||||||
|
|
||||||
|
List<WorkHistoryItemDTO> selectCheckInHistoryByUserIds(
|
||||||
|
@Param("visibleUserIds") List<Long> visibleUserIds,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
|
||||||
@DataScope(tableAlias = "r", ownerColumn = "user_id")
|
@DataScope(tableAlias = "r", ownerColumn = "user_id")
|
||||||
List<WorkHistoryItemDTO> selectReportHistory(@Param("limit") int limit);
|
List<WorkHistoryItemDTO> selectReportHistory(@Param("limit") int limit);
|
||||||
|
|
||||||
|
List<WorkHistoryItemDTO> selectReportHistoryByUserIds(
|
||||||
|
@Param("visibleUserIds") List<Long> visibleUserIds,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
|
||||||
WorkHistoryItemDTO selectReportHistoryById(@Param("userId") Long userId, @Param("reportId") Long reportId);
|
WorkHistoryItemDTO selectReportHistoryById(@Param("userId") Long userId, @Param("reportId") Long reportId);
|
||||||
|
|
||||||
|
WorkHistoryItemDTO selectReportHistoryByIdForUserIds(
|
||||||
|
@Param("reportId") Long reportId,
|
||||||
|
@Param("visibleUserIds") List<Long> visibleUserIds);
|
||||||
|
|
||||||
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
||||||
List<WorkCheckInExportDTO> selectCheckInExportRows(
|
List<WorkCheckInExportDTO> selectCheckInExportRows(
|
||||||
@Param("startDate") LocalDate startDate,
|
@Param("startDate") LocalDate startDate,
|
||||||
|
|
@ -47,6 +59,16 @@ public interface WorkMapper {
|
||||||
@Param("status") String status,
|
@Param("status") String status,
|
||||||
@Param("limit") int limit);
|
@Param("limit") int limit);
|
||||||
|
|
||||||
|
List<WorkCheckInExportDTO> selectCheckInExportRowsByUserIds(
|
||||||
|
@Param("visibleUserIds") List<Long> visibleUserIds,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("deptName") String deptName,
|
||||||
|
@Param("bizType") String bizType,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
|
||||||
@DataScope(tableAlias = "r", ownerColumn = "user_id")
|
@DataScope(tableAlias = "r", ownerColumn = "user_id")
|
||||||
List<WorkDailyReportExportDTO> selectDailyReportExportRows(
|
List<WorkDailyReportExportDTO> selectDailyReportExportRows(
|
||||||
@Param("startDate") LocalDate startDate,
|
@Param("startDate") LocalDate startDate,
|
||||||
|
|
@ -56,6 +78,15 @@ public interface WorkMapper {
|
||||||
@Param("status") String status,
|
@Param("status") String status,
|
||||||
@Param("limit") int limit);
|
@Param("limit") int limit);
|
||||||
|
|
||||||
|
List<WorkDailyReportExportDTO> selectDailyReportExportRowsByUserIds(
|
||||||
|
@Param("visibleUserIds") List<Long> visibleUserIds,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("deptName") String deptName,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
|
||||||
Long selectTodayCheckInId(@Param("userId") Long userId);
|
Long selectTodayCheckInId(@Param("userId") Long userId);
|
||||||
|
|
||||||
int insertCheckIn(@Param("userId") Long userId, @Param("request") CreateWorkCheckInRequest request);
|
int insertCheckIn(@Param("userId") Long userId, @Param("request") CreateWorkCheckInRequest request);
|
||||||
|
|
@ -164,7 +195,8 @@ public interface WorkMapper {
|
||||||
@Param("opportunityId") Long opportunityId,
|
@Param("opportunityId") Long opportunityId,
|
||||||
@Param("latestProgress") String latestProgress,
|
@Param("latestProgress") String latestProgress,
|
||||||
@Param("nextPlan") String nextPlan,
|
@Param("nextPlan") String nextPlan,
|
||||||
@Param("stage") String stage);
|
@Param("stage") String stage,
|
||||||
|
@Param("updatedBy") Long updatedBy);
|
||||||
|
|
||||||
int deleteTodosByBiz(@Param("userId") Long userId, @Param("bizType") String bizType, @Param("bizId") Long bizId);
|
int deleteTodosByBiz(@Param("userId") Long userId, @Param("bizType") String bizType, @Param("bizId") Long bizId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.unis.crm.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CrmDataVisibilityService {
|
||||||
|
|
||||||
|
String RESOURCE_ALL = "ALL";
|
||||||
|
String RESOURCE_OPPORTUNITY = "OPPORTUNITY";
|
||||||
|
String RESOURCE_EXPANSION = "EXPANSION";
|
||||||
|
String RESOURCE_DAILY_REPORT = "DAILY_REPORT";
|
||||||
|
String RESOURCE_CHECKIN = "CHECKIN";
|
||||||
|
|
||||||
|
DataVisibility resolveVisibility(Long currentUserId, Long tenantId, String resourceType);
|
||||||
|
|
||||||
|
record DataVisibility(
|
||||||
|
boolean allDataAccess,
|
||||||
|
List<Long> visibleOwnerUserIds) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
package com.unis.crm.service;
|
||||||
|
|
||||||
|
import com.unis.crm.common.BusinessException;
|
||||||
|
import com.unis.crm.common.UnauthorizedException;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeAssignmentDTO;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeGrantDTO;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeUserDTO;
|
||||||
|
import com.unisbase.security.PermissionService;
|
||||||
|
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||||
|
import com.unisbase.security.SpringSecurityUserProvider;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserDataScopeAdminService {
|
||||||
|
|
||||||
|
private static final String VIEW_PERM = "user_data_scope:view";
|
||||||
|
private static final String UPDATE_PERM = "user_data_scope:update";
|
||||||
|
private static final Set<String> RESOURCE_TYPES = Set.of(
|
||||||
|
"ALL",
|
||||||
|
"OPPORTUNITY",
|
||||||
|
"EXPANSION",
|
||||||
|
"DAILY_REPORT",
|
||||||
|
"CHECKIN",
|
||||||
|
"CUSTOMER",
|
||||||
|
"WORK");
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PermissionService permissionService;
|
||||||
|
private final SpringSecurityTenantProvider tenantProvider;
|
||||||
|
private final SpringSecurityUserProvider userProvider;
|
||||||
|
|
||||||
|
public UserDataScopeAdminService(
|
||||||
|
JdbcTemplate jdbcTemplate,
|
||||||
|
PermissionService permissionService,
|
||||||
|
SpringSecurityTenantProvider tenantProvider,
|
||||||
|
SpringSecurityUserProvider userProvider) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.permissionService = permissionService;
|
||||||
|
this.tenantProvider = tenantProvider;
|
||||||
|
this.userProvider = userProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserDataScopeUserDTO> listUsers(Long tenantId) {
|
||||||
|
Long resolvedTenantId = resolveTenantId(tenantId);
|
||||||
|
requirePermission(VIEW_PERM, "无权查看数据授权配置");
|
||||||
|
return jdbcTemplate.query("""
|
||||||
|
select
|
||||||
|
u.user_id,
|
||||||
|
coalesce(nullif(u.username, ''), cast(u.user_id as varchar)) as username,
|
||||||
|
coalesce(nullif(u.display_name, ''), nullif(u.username, ''), cast(u.user_id as varchar)) as display_name,
|
||||||
|
coalesce(u.status, 1) as status,
|
||||||
|
tu.tenant_id,
|
||||||
|
string_agg(distinct o.org_name, '、' order by o.org_name) as org_name
|
||||||
|
from sys_user u
|
||||||
|
join sys_tenant_user tu on tu.user_id = u.user_id
|
||||||
|
left join sys_org o
|
||||||
|
on o.id = tu.org_id
|
||||||
|
and o.tenant_id = tu.tenant_id
|
||||||
|
and coalesce(o.is_deleted, 0) = 0
|
||||||
|
where tu.tenant_id = ?
|
||||||
|
and coalesce(u.is_deleted, 0) = 0
|
||||||
|
and coalesce(tu.is_deleted, 0) = 0
|
||||||
|
group by u.user_id, u.username, u.display_name, u.status, tu.tenant_id
|
||||||
|
order by coalesce(u.status, 1) desc, display_name asc, username asc, u.user_id asc
|
||||||
|
""", (resultSet, rowNum) -> mapUser(resultSet), resolvedTenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserDataScopeGrantDTO> listGrants(Long tenantId, Long viewerUserId, String resourceType) {
|
||||||
|
Long resolvedTenantId = resolveTenantId(tenantId);
|
||||||
|
requirePermission(VIEW_PERM, "无权查看数据授权配置");
|
||||||
|
String normalizedResourceType = normalizeResourceType(resourceType, false);
|
||||||
|
List<Object> args = new ArrayList<>();
|
||||||
|
args.add(resolvedTenantId);
|
||||||
|
StringBuilder sql = new StringBuilder("""
|
||||||
|
select
|
||||||
|
g.id,
|
||||||
|
g.tenant_id,
|
||||||
|
g.viewer_user_id,
|
||||||
|
coalesce(nullif(viewer.display_name, ''), nullif(viewer.username, ''), cast(g.viewer_user_id as varchar)) as viewer_name,
|
||||||
|
g.owner_user_id,
|
||||||
|
coalesce(nullif(owner_user.display_name, ''), nullif(owner_user.username, ''), cast(g.owner_user_id as varchar)) as owner_name,
|
||||||
|
g.resource_type,
|
||||||
|
g.enabled,
|
||||||
|
g.expire_at,
|
||||||
|
g.remark,
|
||||||
|
g.created_at,
|
||||||
|
g.updated_at
|
||||||
|
from sys_user_data_scope_user g
|
||||||
|
left join sys_user viewer on viewer.user_id = g.viewer_user_id
|
||||||
|
left join sys_user owner_user on owner_user.user_id = g.owner_user_id
|
||||||
|
where g.tenant_id = ?
|
||||||
|
and coalesce(g.is_deleted, 0) = 0
|
||||||
|
""");
|
||||||
|
if (viewerUserId != null && viewerUserId > 0) {
|
||||||
|
sql.append(" and g.viewer_user_id = ?");
|
||||||
|
args.add(viewerUserId);
|
||||||
|
}
|
||||||
|
if (normalizedResourceType != null) {
|
||||||
|
sql.append(" and g.resource_type = ?");
|
||||||
|
args.add(normalizedResourceType);
|
||||||
|
}
|
||||||
|
sql.append(" order by viewer_name asc, g.resource_type asc, owner_name asc, g.id asc");
|
||||||
|
return jdbcTemplate.query(sql.toString(), (resultSet, rowNum) -> mapGrant(resultSet), args.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserDataScopeAssignmentDTO getAssignment(Long tenantId, Long viewerUserId, String resourceType) {
|
||||||
|
Long resolvedTenantId = resolveTenantId(tenantId);
|
||||||
|
requirePermission(VIEW_PERM, "无权查看数据授权配置");
|
||||||
|
Long normalizedViewerUserId = requirePositiveUserId(viewerUserId, "请选择查看人");
|
||||||
|
String normalizedResourceType = normalizeResourceType(resourceType, true);
|
||||||
|
requireUserInTenant(resolvedTenantId, normalizedViewerUserId, "查看人不存在或不属于当前租户");
|
||||||
|
|
||||||
|
List<UserDataScopeGrantDTO> grants = listGrants(resolvedTenantId, normalizedViewerUserId, normalizedResourceType);
|
||||||
|
UserDataScopeAssignmentDTO dto = new UserDataScopeAssignmentDTO();
|
||||||
|
dto.setTenantId(resolvedTenantId);
|
||||||
|
dto.setViewerUserId(normalizedViewerUserId);
|
||||||
|
dto.setResourceType(normalizedResourceType);
|
||||||
|
dto.setResourceTypes(List.of(normalizedResourceType));
|
||||||
|
dto.setOwnerUserIds(grants.stream()
|
||||||
|
.map(UserDataScopeGrantDTO::getOwnerUserId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList());
|
||||||
|
grants.stream().findFirst().ifPresent(first -> {
|
||||||
|
dto.setEnabled(first.getEnabled());
|
||||||
|
dto.setExpireAt(first.getExpireAt());
|
||||||
|
dto.setRemark(first.getRemark());
|
||||||
|
});
|
||||||
|
if (dto.getEnabled() == null) {
|
||||||
|
dto.setEnabled(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public boolean saveAssignment(UserDataScopeAssignmentDTO payload) {
|
||||||
|
requirePermission(UPDATE_PERM, "无权修改数据授权配置");
|
||||||
|
if (payload == null) {
|
||||||
|
throw new BusinessException("请提交数据授权配置");
|
||||||
|
}
|
||||||
|
Long tenantId = resolveTenantId(payload.getTenantId());
|
||||||
|
Long viewerUserId = requirePositiveUserId(payload.getViewerUserId(), "请选择查看人");
|
||||||
|
List<String> resourceTypes = normalizeResourceTypes(payload.getResourceTypes(), payload.getResourceType());
|
||||||
|
requireUserInTenant(tenantId, viewerUserId, "查看人不存在或不属于当前租户");
|
||||||
|
|
||||||
|
List<Long> ownerUserIds = normalizeOwnerUserIds(payload.getOwnerUserIds(), viewerUserId);
|
||||||
|
validateOwnersInTenant(tenantId, ownerUserIds);
|
||||||
|
boolean enabled = payload.getEnabled() == null || Boolean.TRUE.equals(payload.getEnabled());
|
||||||
|
Long createdBy = userProvider == null ? null : userProvider.getCurrentUserId();
|
||||||
|
|
||||||
|
for (String resourceType : resourceTypes) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
update sys_user_data_scope_user
|
||||||
|
set enabled = false,
|
||||||
|
is_deleted = 1,
|
||||||
|
updated_at = now()
|
||||||
|
where tenant_id = ?
|
||||||
|
and viewer_user_id = ?
|
||||||
|
and resource_type = ?
|
||||||
|
and coalesce(is_deleted, 0) = 0
|
||||||
|
""", tenantId, viewerUserId, resourceType);
|
||||||
|
|
||||||
|
for (Long ownerUserId : ownerUserIds) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
insert into sys_user_data_scope_user (
|
||||||
|
tenant_id,
|
||||||
|
viewer_user_id,
|
||||||
|
owner_user_id,
|
||||||
|
resource_type,
|
||||||
|
enabled,
|
||||||
|
expire_at,
|
||||||
|
remark,
|
||||||
|
created_by,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
is_deleted
|
||||||
|
) values (?, ?, ?, ?, ?, ?, ?, ?, now(), now(), 0)
|
||||||
|
""",
|
||||||
|
tenantId,
|
||||||
|
viewerUserId,
|
||||||
|
ownerUserId,
|
||||||
|
resourceType,
|
||||||
|
enabled,
|
||||||
|
payload.getExpireAt(),
|
||||||
|
trimToNull(payload.getRemark()),
|
||||||
|
createdBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public boolean deleteGrant(Long tenantId, Long grantId) {
|
||||||
|
requirePermission(UPDATE_PERM, "无权修改数据授权配置");
|
||||||
|
Long resolvedTenantId = resolveTenantId(tenantId);
|
||||||
|
if (grantId == null || grantId <= 0) {
|
||||||
|
throw new BusinessException("数据授权记录不存在");
|
||||||
|
}
|
||||||
|
int updated = jdbcTemplate.update("""
|
||||||
|
update sys_user_data_scope_user
|
||||||
|
set enabled = false,
|
||||||
|
is_deleted = 1,
|
||||||
|
updated_at = now()
|
||||||
|
where id = ?
|
||||||
|
and tenant_id = ?
|
||||||
|
and coalesce(is_deleted, 0) = 0
|
||||||
|
""", grantId, resolvedTenantId);
|
||||||
|
if (updated <= 0) {
|
||||||
|
throw new BusinessException("数据授权记录不存在或已删除");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDataScopeUserDTO mapUser(ResultSet resultSet) throws SQLException {
|
||||||
|
UserDataScopeUserDTO dto = new UserDataScopeUserDTO();
|
||||||
|
dto.setUserId(resultSet.getLong("user_id"));
|
||||||
|
dto.setUsername(resultSet.getString("username"));
|
||||||
|
dto.setDisplayName(resultSet.getString("display_name"));
|
||||||
|
dto.setStatus(resultSet.getInt("status"));
|
||||||
|
dto.setTenantId(resultSet.getLong("tenant_id"));
|
||||||
|
dto.setOrgName(resultSet.getString("org_name"));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDataScopeGrantDTO mapGrant(ResultSet resultSet) throws SQLException {
|
||||||
|
UserDataScopeGrantDTO dto = new UserDataScopeGrantDTO();
|
||||||
|
dto.setId(resultSet.getLong("id"));
|
||||||
|
dto.setTenantId(resultSet.getLong("tenant_id"));
|
||||||
|
dto.setViewerUserId(resultSet.getLong("viewer_user_id"));
|
||||||
|
dto.setViewerName(resultSet.getString("viewer_name"));
|
||||||
|
dto.setOwnerUserId(resultSet.getLong("owner_user_id"));
|
||||||
|
dto.setOwnerName(resultSet.getString("owner_name"));
|
||||||
|
dto.setResourceType(resultSet.getString("resource_type"));
|
||||||
|
dto.setEnabled(resultSet.getBoolean("enabled"));
|
||||||
|
dto.setExpireAt(resultSet.getObject("expire_at", OffsetDateTime.class));
|
||||||
|
dto.setRemark(resultSet.getString("remark"));
|
||||||
|
dto.setCreatedAt(resultSet.getObject("created_at", OffsetDateTime.class));
|
||||||
|
dto.setUpdatedAt(resultSet.getObject("updated_at", OffsetDateTime.class));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveTenantId(Long requestedTenantId) {
|
||||||
|
Long currentTenantId = tenantProvider == null ? null : tenantProvider.getCurrentTenantId();
|
||||||
|
if (currentTenantId != null && currentTenantId > 0) {
|
||||||
|
if (requestedTenantId != null && requestedTenantId > 0 && !currentTenantId.equals(requestedTenantId)) {
|
||||||
|
throw new BusinessException("只能配置当前租户的数据授权");
|
||||||
|
}
|
||||||
|
return currentTenantId;
|
||||||
|
}
|
||||||
|
if (requestedTenantId != null && requestedTenantId > 0) {
|
||||||
|
return requestedTenantId;
|
||||||
|
}
|
||||||
|
throw new BusinessException("请先切换到具体租户后再配置数据授权");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requirePermission(String perm, String message) {
|
||||||
|
if (!permissionService.hasPermi(perm)) {
|
||||||
|
throw new UnauthorizedException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long requirePositiveUserId(Long userId, String message) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new BusinessException(message);
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireUserInTenant(Long tenantId, Long userId, String message) {
|
||||||
|
Integer count = jdbcTemplate.queryForObject("""
|
||||||
|
select count(1)
|
||||||
|
from sys_user u
|
||||||
|
join sys_tenant_user tu on tu.user_id = u.user_id
|
||||||
|
where tu.tenant_id = ?
|
||||||
|
and u.user_id = ?
|
||||||
|
and coalesce(u.is_deleted, 0) = 0
|
||||||
|
and coalesce(tu.is_deleted, 0) = 0
|
||||||
|
""", Integer.class, tenantId, userId);
|
||||||
|
if (count == null || count <= 0) {
|
||||||
|
throw new BusinessException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOwnersInTenant(Long tenantId, List<Long> ownerUserIds) {
|
||||||
|
for (Long ownerUserId : ownerUserIds) {
|
||||||
|
requireUserInTenant(tenantId, ownerUserId, "可见人员不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> normalizeOwnerUserIds(List<Long> ownerUserIds, Long viewerUserId) {
|
||||||
|
if (ownerUserIds == null || ownerUserIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Set<Long> normalized = new LinkedHashSet<>();
|
||||||
|
for (Long ownerUserId : ownerUserIds) {
|
||||||
|
if (ownerUserId != null && ownerUserId > 0 && !ownerUserId.equals(viewerUserId)) {
|
||||||
|
normalized.add(ownerUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeResourceType(String resourceType, boolean required) {
|
||||||
|
String normalized = trimToNull(resourceType);
|
||||||
|
if (normalized == null) {
|
||||||
|
if (required) {
|
||||||
|
throw new BusinessException("请选择授权资源范围");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
normalized = normalized.toUpperCase(Locale.ROOT);
|
||||||
|
if (!RESOURCE_TYPES.contains(normalized)) {
|
||||||
|
throw new BusinessException("不支持的授权资源范围");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeResourceTypes(List<String> resourceTypes, String fallbackResourceType) {
|
||||||
|
Set<String> normalized = new LinkedHashSet<>();
|
||||||
|
if (resourceTypes != null) {
|
||||||
|
for (String resourceType : resourceTypes) {
|
||||||
|
String value = normalizeResourceType(resourceType, false);
|
||||||
|
if (value != null) {
|
||||||
|
normalized.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
normalized.add(normalizeResourceType(fallbackResourceType, true));
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.unis.crm.service.impl;
|
||||||
|
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unisbase.dto.DataScopeRuleDTO;
|
||||||
|
import com.unisbase.service.DataScopeService;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CrmDataVisibilityServiceImpl implements CrmDataVisibilityService {
|
||||||
|
|
||||||
|
private final DataScopeService dataScopeService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
public CrmDataVisibilityServiceImpl(DataScopeService dataScopeService, JdbcTemplate jdbcTemplate) {
|
||||||
|
this.dataScopeService = dataScopeService;
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataVisibility resolveVisibility(Long currentUserId, Long tenantId, String resourceType) {
|
||||||
|
if (currentUserId == null || currentUserId <= 0) {
|
||||||
|
return new DataVisibility(false, List.of());
|
||||||
|
}
|
||||||
|
if (tenantId == null || tenantId <= 0 || dataScopeService == null) {
|
||||||
|
return new DataVisibility(false, List.of(currentUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
DataScopeRuleDTO baseRule = dataScopeService.resolveUserScope(currentUserId, tenantId);
|
||||||
|
if (baseRule != null && baseRule.isAllAccess()) {
|
||||||
|
return new DataVisibility(true, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> visibleOwnerUserIds = new LinkedHashSet<>();
|
||||||
|
if (baseRule == null) {
|
||||||
|
visibleOwnerUserIds.add(currentUserId);
|
||||||
|
} else {
|
||||||
|
addValidUserIds(visibleOwnerUserIds, baseRule.getCreatorUserIds());
|
||||||
|
}
|
||||||
|
addValidUserIds(visibleOwnerUserIds, queryExplicitVisibleOwnerUserIds(currentUserId, tenantId, resourceType));
|
||||||
|
|
||||||
|
return new DataVisibility(false, List.copyOf(visibleOwnerUserIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> queryExplicitVisibleOwnerUserIds(Long currentUserId, Long tenantId, String resourceType) {
|
||||||
|
String normalizedResourceType = normalizeResourceType(resourceType);
|
||||||
|
return jdbcTemplate.queryForList("""
|
||||||
|
select distinct owner_user_id
|
||||||
|
from sys_user_data_scope_user
|
||||||
|
where tenant_id = ?
|
||||||
|
and viewer_user_id = ?
|
||||||
|
and owner_user_id > 0
|
||||||
|
and enabled = true
|
||||||
|
and coalesce(is_deleted, 0) = 0
|
||||||
|
and (expire_at is null or expire_at > now())
|
||||||
|
and resource_type in (?, ?)
|
||||||
|
order by owner_user_id asc
|
||||||
|
""",
|
||||||
|
Long.class,
|
||||||
|
tenantId,
|
||||||
|
currentUserId,
|
||||||
|
RESOURCE_ALL,
|
||||||
|
normalizedResourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addValidUserIds(Set<Long> target, List<Long> source) {
|
||||||
|
if (source == null || source.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Long userId : source) {
|
||||||
|
if (userId != null && userId > 0) {
|
||||||
|
target.add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeResourceType(String resourceType) {
|
||||||
|
if (resourceType == null || resourceType.trim().isEmpty()) {
|
||||||
|
return RESOURCE_ALL;
|
||||||
|
}
|
||||||
|
return resourceType.trim().toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ import com.unis.crm.dto.expansion.SalesExpansionItemDTO;
|
||||||
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
|
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
|
||||||
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
|
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
|
||||||
import com.unis.crm.mapper.ExpansionMapper;
|
import com.unis.crm.mapper.ExpansionMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unis.crm.service.ExpansionService;
|
import com.unis.crm.service.ExpansionService;
|
||||||
import com.unisbase.security.PermissionService;
|
import com.unisbase.security.PermissionService;
|
||||||
import com.unisbase.security.SpringSecurityTenantProvider;
|
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||||
|
|
@ -57,14 +59,17 @@ public class ExpansionServiceImpl implements ExpansionService {
|
||||||
private final ExpansionMapper expansionMapper;
|
private final ExpansionMapper expansionMapper;
|
||||||
private final SpringSecurityTenantProvider tenantProvider;
|
private final SpringSecurityTenantProvider tenantProvider;
|
||||||
private final PermissionService permissionService;
|
private final PermissionService permissionService;
|
||||||
|
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||||
|
|
||||||
public ExpansionServiceImpl(
|
public ExpansionServiceImpl(
|
||||||
ExpansionMapper expansionMapper,
|
ExpansionMapper expansionMapper,
|
||||||
SpringSecurityTenantProvider tenantProvider,
|
SpringSecurityTenantProvider tenantProvider,
|
||||||
PermissionService permissionService) {
|
PermissionService permissionService,
|
||||||
|
CrmDataVisibilityService crmDataVisibilityService) {
|
||||||
this.expansionMapper = expansionMapper;
|
this.expansionMapper = expansionMapper;
|
||||||
this.tenantProvider = tenantProvider;
|
this.tenantProvider = tenantProvider;
|
||||||
this.permissionService = permissionService;
|
this.permissionService = permissionService;
|
||||||
|
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -102,8 +107,17 @@ public class ExpansionServiceImpl implements ExpansionService {
|
||||||
@Override
|
@Override
|
||||||
public ExpansionOverviewDTO getOverview(Long userId, String keyword) {
|
public ExpansionOverviewDTO getOverview(Long userId, String keyword) {
|
||||||
String normalizedKeyword = normalizeKeyword(keyword);
|
String normalizedKeyword = normalizeKeyword(keyword);
|
||||||
List<SalesExpansionItemDTO> salesItems = expansionMapper.selectSalesExpansions(userId, normalizedKeyword);
|
List<Long> extraVisibleOwnerUserIds = resolveExtraVisibleOwnerUserIds(userId);
|
||||||
List<ChannelExpansionItemDTO> channelItems = expansionMapper.selectChannelExpansions(userId, normalizedKeyword);
|
List<SalesExpansionItemDTO> salesItems = mergeSalesExpansionItems(
|
||||||
|
expansionMapper.selectSalesExpansions(userId, normalizedKeyword),
|
||||||
|
extraVisibleOwnerUserIds.isEmpty()
|
||||||
|
? List.of()
|
||||||
|
: expansionMapper.selectSalesExpansionsByOwnerUserIds(extraVisibleOwnerUserIds, normalizedKeyword));
|
||||||
|
List<ChannelExpansionItemDTO> channelItems = mergeChannelExpansionItems(
|
||||||
|
expansionMapper.selectChannelExpansions(userId, normalizedKeyword),
|
||||||
|
extraVisibleOwnerUserIds.isEmpty()
|
||||||
|
? List.of()
|
||||||
|
: expansionMapper.selectChannelExpansionsByOwnerUserIds(extraVisibleOwnerUserIds, normalizedKeyword));
|
||||||
|
|
||||||
attachSalesFollowUps(userId, salesItems);
|
attachSalesFollowUps(userId, salesItems);
|
||||||
attachSalesRelatedProjects(userId, salesItems);
|
attachSalesRelatedProjects(userId, salesItems);
|
||||||
|
|
@ -115,6 +129,64 @@ public class ExpansionServiceImpl implements ExpansionService {
|
||||||
return new ExpansionOverviewDTO(salesItems, channelItems);
|
return new ExpansionOverviewDTO(salesItems, channelItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Long> resolveExtraVisibleOwnerUserIds(Long userId) {
|
||||||
|
if (userId == null || userId <= 0 || crmDataVisibilityService == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Long tenantId = tenantProvider == null ? null : tenantProvider.getCurrentTenantId();
|
||||||
|
if (tenantId == null || tenantId <= 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
DataVisibility visibility = crmDataVisibilityService.resolveVisibility(
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
CrmDataVisibilityService.RESOURCE_EXPANSION);
|
||||||
|
if (visibility.allDataAccess()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
LinkedHashSet<Long> normalized = new LinkedHashSet<>();
|
||||||
|
for (Long ownerUserId : visibility.visibleOwnerUserIds()) {
|
||||||
|
if (ownerUserId != null && ownerUserId > 0 && !ownerUserId.equals(userId)) {
|
||||||
|
normalized.add(ownerUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SalesExpansionItemDTO> mergeSalesExpansionItems(
|
||||||
|
List<SalesExpansionItemDTO> scopedItems,
|
||||||
|
List<SalesExpansionItemDTO> extraItems) {
|
||||||
|
Map<Long, SalesExpansionItemDTO> byId = new LinkedHashMap<>();
|
||||||
|
for (SalesExpansionItemDTO item : scopedItems) {
|
||||||
|
if (item != null && item.getId() != null) {
|
||||||
|
byId.putIfAbsent(item.getId(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SalesExpansionItemDTO item : extraItems) {
|
||||||
|
if (item != null && item.getId() != null) {
|
||||||
|
byId.putIfAbsent(item.getId(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(byId.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChannelExpansionItemDTO> mergeChannelExpansionItems(
|
||||||
|
List<ChannelExpansionItemDTO> scopedItems,
|
||||||
|
List<ChannelExpansionItemDTO> extraItems) {
|
||||||
|
Map<Long, ChannelExpansionItemDTO> byId = new LinkedHashMap<>();
|
||||||
|
for (ChannelExpansionItemDTO item : scopedItems) {
|
||||||
|
if (item != null && item.getId() != null) {
|
||||||
|
byId.putIfAbsent(item.getId(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ChannelExpansionItemDTO item : extraItems) {
|
||||||
|
if (item != null && item.getId() != null) {
|
||||||
|
byId.putIfAbsent(item.getId(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(byId.values());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExpansionOverviewDTO getOpportunityFormOptions(Long userId, String keyword, Integer limit) {
|
public ExpansionOverviewDTO getOpportunityFormOptions(Long userId, String keyword, Integer limit) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
|
||||||
import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest;
|
import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest;
|
||||||
import com.unis.crm.dto.work.WorkReportAttachmentDTO;
|
import com.unis.crm.dto.work.WorkReportAttachmentDTO;
|
||||||
import com.unis.crm.mapper.OpportunityMapper;
|
import com.unis.crm.mapper.OpportunityMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unis.crm.service.OmsClient;
|
import com.unis.crm.service.OmsClient;
|
||||||
import com.unis.crm.service.OpportunityService;
|
import com.unis.crm.service.OpportunityService;
|
||||||
import com.unisbase.dto.DataScopeRuleDTO;
|
|
||||||
import com.unisbase.security.PermissionService;
|
import com.unisbase.security.PermissionService;
|
||||||
import com.unisbase.service.DataScopeService;
|
|
||||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
@ -63,7 +63,7 @@ public class OpportunityServiceImpl implements OpportunityService {
|
||||||
private final OpportunityMapper opportunityMapper;
|
private final OpportunityMapper opportunityMapper;
|
||||||
private final OmsClient omsClient;
|
private final OmsClient omsClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final DataScopeService dataScopeService;
|
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||||
private final UnisBaseTenantProvider tenantProvider;
|
private final UnisBaseTenantProvider tenantProvider;
|
||||||
private final PermissionService permissionService;
|
private final PermissionService permissionService;
|
||||||
|
|
||||||
|
|
@ -71,13 +71,13 @@ public class OpportunityServiceImpl implements OpportunityService {
|
||||||
OpportunityMapper opportunityMapper,
|
OpportunityMapper opportunityMapper,
|
||||||
OmsClient omsClient,
|
OmsClient omsClient,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
DataScopeService dataScopeService,
|
CrmDataVisibilityService crmDataVisibilityService,
|
||||||
UnisBaseTenantProvider tenantProvider,
|
UnisBaseTenantProvider tenantProvider,
|
||||||
PermissionService permissionService) {
|
PermissionService permissionService) {
|
||||||
this.opportunityMapper = opportunityMapper;
|
this.opportunityMapper = opportunityMapper;
|
||||||
this.omsClient = omsClient;
|
this.omsClient = omsClient;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.dataScopeService = dataScopeService;
|
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||||
this.tenantProvider = tenantProvider;
|
this.tenantProvider = tenantProvider;
|
||||||
this.permissionService = permissionService;
|
this.permissionService = permissionService;
|
||||||
}
|
}
|
||||||
|
|
@ -318,18 +318,19 @@ public class OpportunityServiceImpl implements OpportunityService {
|
||||||
}
|
}
|
||||||
List<String> preSalesUserNames = resolveCurrentUserPreSalesNames(userId);
|
List<String> preSalesUserNames = resolveCurrentUserPreSalesNames(userId);
|
||||||
Long tenantId = tenantProvider == null ? null : tenantProvider.getCurrentTenantId();
|
Long tenantId = tenantProvider == null ? null : tenantProvider.getCurrentTenantId();
|
||||||
if (tenantId == null || dataScopeService == null) {
|
if (tenantId == null || crmDataVisibilityService == null) {
|
||||||
return new OpportunityVisibility(false, List.of(userId), userId, preSalesUserNames);
|
return new OpportunityVisibility(false, List.of(userId), userId, preSalesUserNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
DataScopeRuleDTO rule = dataScopeService.resolveUserScope(userId, tenantId);
|
DataVisibility visibility = crmDataVisibilityService.resolveVisibility(
|
||||||
if (rule == null) {
|
userId,
|
||||||
return new OpportunityVisibility(false, List.of(userId), userId, preSalesUserNames);
|
tenantId,
|
||||||
}
|
CrmDataVisibilityService.RESOURCE_OPPORTUNITY);
|
||||||
if (rule.isAllAccess()) {
|
return new OpportunityVisibility(
|
||||||
return new OpportunityVisibility(true, List.of(), userId, preSalesUserNames);
|
visibility.allDataAccess(),
|
||||||
}
|
normalizeVisibleOwnerUserIds(visibility.visibleOwnerUserIds()),
|
||||||
return new OpportunityVisibility(false, normalizeVisibleOwnerUserIds(rule.getCreatorUserIds()), userId, preSalesUserNames);
|
userId,
|
||||||
|
preSalesUserNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> resolveCurrentUserPreSalesNames(Long userId) {
|
private List<String> resolveCurrentUserPreSalesNames(Long userId) {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,11 @@ import com.unis.crm.dto.work.WorkSuggestedActionDTO;
|
||||||
import com.unis.crm.mapper.OpportunityMapper;
|
import com.unis.crm.mapper.OpportunityMapper;
|
||||||
import com.unis.crm.mapper.ProfileMapper;
|
import com.unis.crm.mapper.ProfileMapper;
|
||||||
import com.unis.crm.mapper.WorkMapper;
|
import com.unis.crm.mapper.WorkMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unis.crm.service.ReportReminderService;
|
import com.unis.crm.service.ReportReminderService;
|
||||||
import com.unis.crm.service.WorkService;
|
import com.unis.crm.service.WorkService;
|
||||||
|
import com.unisbase.service.SysPermissionService;
|
||||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
@ -49,9 +52,11 @@ import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
@ -59,6 +64,7 @@ import java.util.regex.Pattern;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.UrlResource;
|
import org.springframework.core.io.UrlResource;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
|
@ -83,6 +89,9 @@ public class WorkServiceImpl implements WorkService {
|
||||||
private static final String REPORT_ATTACHMENT_METADATA_SUFFIX = "[[/WORK_REPORT_ATTACHMENTS]]";
|
private static final String REPORT_ATTACHMENT_METADATA_SUFFIX = "[[/WORK_REPORT_ATTACHMENTS]]";
|
||||||
private static final Pattern REPORT_ATTACHMENT_METADATA_PATTERN = Pattern.compile("\\[\\[WORK_REPORT_ATTACHMENTS]](.*?)\\[\\[/WORK_REPORT_ATTACHMENTS]]", Pattern.DOTALL);
|
private static final Pattern REPORT_ATTACHMENT_METADATA_PATTERN = Pattern.compile("\\[\\[WORK_REPORT_ATTACHMENTS]](.*?)\\[\\[/WORK_REPORT_ATTACHMENTS]]", Pattern.DOTALL);
|
||||||
private static final String ONLY_SEE_ROLE_CODE = "only_see";
|
private static final String ONLY_SEE_ROLE_CODE = "only_see";
|
||||||
|
private static final String SALES_ROLE_CODE = "xs_col";
|
||||||
|
private static final String DAILY_REPORT_NOTIFY_USERS_PERMISSION = "daily_report:notify_users";
|
||||||
|
private static final String DAILY_REPORT_ATTACHMENTS_PERMISSION = "daily_report:attachments";
|
||||||
private static final String WORK_REPORT_FOLLOW_UP_TYPE = "工作日报";
|
private static final String WORK_REPORT_FOLLOW_UP_TYPE = "工作日报";
|
||||||
private static final String LEGACY_NEXT_PLAN_LABEL = "后续规划";
|
private static final String LEGACY_NEXT_PLAN_LABEL = "后续规划";
|
||||||
private static final String OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
|
private static final String OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
|
||||||
|
|
@ -105,8 +114,11 @@ public class WorkServiceImpl implements WorkService {
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final UnisBaseTenantProvider tenantProvider;
|
private final UnisBaseTenantProvider tenantProvider;
|
||||||
|
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||||
private final ReportReminderService reportReminderService;
|
private final ReportReminderService reportReminderService;
|
||||||
private final WorkReportProperties workReportProperties;
|
private final WorkReportProperties workReportProperties;
|
||||||
|
private final SysPermissionService sysPermissionService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final Path checkInPhotoDirectory;
|
private final Path checkInPhotoDirectory;
|
||||||
private final Path reportAttachmentDirectory;
|
private final Path reportAttachmentDirectory;
|
||||||
private final String tencentMapKey;
|
private final String tencentMapKey;
|
||||||
|
|
@ -120,6 +132,9 @@ public class WorkServiceImpl implements WorkService {
|
||||||
WorkReportProperties workReportProperties,
|
WorkReportProperties workReportProperties,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
UnisBaseTenantProvider tenantProvider,
|
UnisBaseTenantProvider tenantProvider,
|
||||||
|
CrmDataVisibilityService crmDataVisibilityService,
|
||||||
|
SysPermissionService sysPermissionService,
|
||||||
|
JdbcTemplate jdbcTemplate,
|
||||||
@Value("${unisbase.app.upload-path}") String uploadPath,
|
@Value("${unisbase.app.upload-path}") String uploadPath,
|
||||||
@Value("${unisbase.app.tencent-map.key:}") String tencentMapKey) {
|
@Value("${unisbase.app.tencent-map.key:}") String tencentMapKey) {
|
||||||
this.workMapper = workMapper;
|
this.workMapper = workMapper;
|
||||||
|
|
@ -129,6 +144,9 @@ public class WorkServiceImpl implements WorkService {
|
||||||
this.workReportProperties = workReportProperties == null ? new WorkReportProperties() : workReportProperties;
|
this.workReportProperties = workReportProperties == null ? new WorkReportProperties() : workReportProperties;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.tenantProvider = tenantProvider;
|
this.tenantProvider = tenantProvider;
|
||||||
|
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||||
|
this.sysPermissionService = sysPermissionService;
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.httpClient = HttpClient.newBuilder()
|
this.httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(8))
|
.connectTimeout(Duration.ofSeconds(8))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -151,7 +169,7 @@ public class WorkServiceImpl implements WorkService {
|
||||||
@Override
|
@Override
|
||||||
public List<WorkReportToUserDTO> listReportMessageUsers(Long userId, String keyword) {
|
public List<WorkReportToUserDTO> listReportMessageUsers(Long userId, String keyword) {
|
||||||
requireUser(userId);
|
requireUser(userId);
|
||||||
if (!isDailyReportNotifyUsersVisible(loadUserRoleCodes(userId))) {
|
if (!isDailyReportNotifyUsersVisible(userId, loadUserRoleCodes(userId))) {
|
||||||
throw new BusinessException("当前角色不可使用日报通知用户功能");
|
throw new BusinessException("当前角色不可使用日报通知用户功能");
|
||||||
}
|
}
|
||||||
return workMapper.selectReportMessageUsers(resolveCurrentTenantId(), normalizeOptionalText(keyword), 50);
|
return workMapper.selectReportMessageUsers(resolveCurrentTenantId(), normalizeOptionalText(keyword), 50);
|
||||||
|
|
@ -163,8 +181,8 @@ public class WorkServiceImpl implements WorkService {
|
||||||
List<String> roleCodes = loadUserRoleCodes(userId);
|
List<String> roleCodes = loadUserRoleCodes(userId);
|
||||||
return new WorkDailyReportRequirementDTO(
|
return new WorkDailyReportRequirementDTO(
|
||||||
isDailyReportObjectSelectionRequired(roleCodes),
|
isDailyReportObjectSelectionRequired(roleCodes),
|
||||||
isDailyReportNotifyUsersVisible(roleCodes),
|
isDailyReportNotifyUsersVisible(userId, roleCodes),
|
||||||
isDailyReportAttachmentsVisible(roleCodes));
|
isDailyReportAttachmentsVisible(userId, roleCodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -175,7 +193,7 @@ public class WorkServiceImpl implements WorkService {
|
||||||
int offset = (safePage - 1) * safeSize;
|
int offset = (safePage - 1) * safeSize;
|
||||||
HistoryType historyType = mapHistoryType(type);
|
HistoryType historyType = mapHistoryType(type);
|
||||||
int fetchLimit = offset + safeSize + 1;
|
int fetchLimit = offset + safeSize + 1;
|
||||||
List<WorkHistoryItemDTO> historyItems = loadHistoryItems(historyType, fetchLimit);
|
List<WorkHistoryItemDTO> historyItems = loadHistoryItems(userId, historyType, fetchLimit);
|
||||||
|
|
||||||
boolean hasMore = historyItems.size() > offset + safeSize;
|
boolean hasMore = historyItems.size() > offset + safeSize;
|
||||||
if (offset >= historyItems.size()) {
|
if (offset >= historyItems.size()) {
|
||||||
|
|
@ -195,6 +213,12 @@ public class WorkServiceImpl implements WorkService {
|
||||||
throw new BusinessException("日报不存在");
|
throw new BusinessException("日报不存在");
|
||||||
}
|
}
|
||||||
WorkHistoryItemDTO item = workMapper.selectReportHistoryById(userId, reportId);
|
WorkHistoryItemDTO item = workMapper.selectReportHistoryById(userId, reportId);
|
||||||
|
if (item == null) {
|
||||||
|
List<Long> visibleUserIds = resolveExtraVisibleUserIds(userId, CrmDataVisibilityService.RESOURCE_DAILY_REPORT);
|
||||||
|
if (!visibleUserIds.isEmpty()) {
|
||||||
|
item = workMapper.selectReportHistoryByIdForUserIds(reportId, visibleUserIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
throw new BusinessException("日报不存在或无权查看");
|
throw new BusinessException("日报不存在或无权查看");
|
||||||
}
|
}
|
||||||
|
|
@ -214,14 +238,29 @@ public class WorkServiceImpl implements WorkService {
|
||||||
requireUser(userId);
|
requireUser(userId);
|
||||||
validateDateRange(startDate, endDate);
|
validateDateRange(startDate, endDate);
|
||||||
String normalizedBizType = normalizeBizType(bizType);
|
String normalizedBizType = normalizeBizType(bizType);
|
||||||
List<WorkCheckInExportDTO> rows = workMapper.selectCheckInExportRows(
|
List<Long> extraVisibleUserIds = resolveExtraVisibleUserIds(
|
||||||
startDate,
|
userId,
|
||||||
endDate,
|
CrmDataVisibilityService.RESOURCE_CHECKIN);
|
||||||
normalizeOptionalText(keyword),
|
List<WorkCheckInExportDTO> rows = limitMergedWorkCheckInExportRows(
|
||||||
normalizeOptionalText(deptName),
|
workMapper.selectCheckInExportRows(
|
||||||
normalizedBizType,
|
startDate,
|
||||||
normalizeOptionalText(status),
|
endDate,
|
||||||
EXPORT_LIMIT);
|
normalizeOptionalText(keyword),
|
||||||
|
normalizeOptionalText(deptName),
|
||||||
|
normalizedBizType,
|
||||||
|
normalizeOptionalText(status),
|
||||||
|
EXPORT_LIMIT),
|
||||||
|
extraVisibleUserIds.isEmpty()
|
||||||
|
? List.of()
|
||||||
|
: workMapper.selectCheckInExportRowsByUserIds(
|
||||||
|
extraVisibleUserIds,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
normalizeOptionalText(keyword),
|
||||||
|
normalizeOptionalText(deptName),
|
||||||
|
normalizedBizType,
|
||||||
|
normalizeOptionalText(status),
|
||||||
|
EXPORT_LIMIT));
|
||||||
normalizeCheckInExportRows(rows);
|
normalizeCheckInExportRows(rows);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
@ -238,13 +277,27 @@ public class WorkServiceImpl implements WorkService {
|
||||||
requireUser(userId);
|
requireUser(userId);
|
||||||
validateDateRange(startDate, endDate);
|
validateDateRange(startDate, endDate);
|
||||||
String normalizedBizType = normalizeBizType(bizType);
|
String normalizedBizType = normalizeBizType(bizType);
|
||||||
List<WorkDailyReportExportDTO> rows = workMapper.selectDailyReportExportRows(
|
List<Long> extraVisibleUserIds = resolveExtraVisibleUserIds(
|
||||||
startDate,
|
userId,
|
||||||
endDate,
|
CrmDataVisibilityService.RESOURCE_DAILY_REPORT);
|
||||||
normalizeOptionalText(keyword),
|
List<WorkDailyReportExportDTO> rows = limitMergedWorkDailyReportExportRows(
|
||||||
normalizeOptionalText(deptName),
|
workMapper.selectDailyReportExportRows(
|
||||||
normalizeOptionalText(status),
|
startDate,
|
||||||
EXPORT_LIMIT);
|
endDate,
|
||||||
|
normalizeOptionalText(keyword),
|
||||||
|
normalizeOptionalText(deptName),
|
||||||
|
normalizeOptionalText(status),
|
||||||
|
EXPORT_LIMIT),
|
||||||
|
extraVisibleUserIds.isEmpty()
|
||||||
|
? List.of()
|
||||||
|
: workMapper.selectDailyReportExportRowsByUserIds(
|
||||||
|
extraVisibleUserIds,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
normalizeOptionalText(keyword),
|
||||||
|
normalizeOptionalText(deptName),
|
||||||
|
normalizeOptionalText(status),
|
||||||
|
EXPORT_LIMIT));
|
||||||
normalizeDailyReportExportRows(rows);
|
normalizeDailyReportExportRows(rows);
|
||||||
if (normalizedBizType == null) {
|
if (normalizedBizType == null) {
|
||||||
return rows;
|
return rows;
|
||||||
|
|
@ -294,8 +347,8 @@ public class WorkServiceImpl implements WorkService {
|
||||||
normalizeDailyReportRequest(
|
normalizeDailyReportRequest(
|
||||||
request,
|
request,
|
||||||
isDailyReportObjectSelectionRequired(roleCodes),
|
isDailyReportObjectSelectionRequired(roleCodes),
|
||||||
isDailyReportNotifyUsersVisible(roleCodes),
|
isDailyReportNotifyUsersVisible(userId, roleCodes),
|
||||||
isDailyReportAttachmentsVisible(roleCodes));
|
isDailyReportAttachmentsVisible(userId, roleCodes));
|
||||||
request.setSourceType(normalizeOptionalText(request.getSourceType()));
|
request.setSourceType(normalizeOptionalText(request.getSourceType()));
|
||||||
if (request.getSourceType() == null) {
|
if (request.getSourceType() == null) {
|
||||||
request.setSourceType("manual");
|
request.setSourceType("manual");
|
||||||
|
|
@ -327,7 +380,8 @@ public class WorkServiceImpl implements WorkService {
|
||||||
reportId,
|
reportId,
|
||||||
previousReport == null ? List.of() : previousReport.getLineItems(),
|
previousReport == null ? List.of() : previousReport.getLineItems(),
|
||||||
request,
|
request,
|
||||||
currentReport);
|
currentReport,
|
||||||
|
isSalesRole(roleCodes));
|
||||||
syncTomorrowPlanTodo(userId, reportId, request.getPlanItems());
|
syncTomorrowPlanTodo(userId, reportId, request.getPlanItems());
|
||||||
syncReportMessages(userId, reportId, resolveReportDate(currentReport), request);
|
syncReportMessages(userId, reportId, resolveReportDate(currentReport), request);
|
||||||
Long finalReportId = reportId;
|
Long finalReportId = reportId;
|
||||||
|
|
@ -427,7 +481,7 @@ public class WorkServiceImpl implements WorkService {
|
||||||
public WorkReportAttachmentDTO uploadReportAttachment(Long userId, MultipartFile file) {
|
public WorkReportAttachmentDTO uploadReportAttachment(Long userId, MultipartFile file) {
|
||||||
requireUser(userId);
|
requireUser(userId);
|
||||||
List<String> roleCodes = ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
|
List<String> roleCodes = ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
|
||||||
if (!isDailyReportAttachmentsVisible(roleCodes)) {
|
if (!isDailyReportAttachmentsVisible(userId, roleCodes)) {
|
||||||
throw new BusinessException("当前角色不可使用日报附件功能");
|
throw new BusinessException("当前角色不可使用日报附件功能");
|
||||||
}
|
}
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
|
|
@ -528,12 +582,24 @@ public class WorkServiceImpl implements WorkService {
|
||||||
return dailyReport.isRequiredByDefault();
|
return dailyReport.isRequiredByDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDailyReportNotifyUsersVisible(List<String> roleCodes) {
|
private boolean isDailyReportNotifyUsersVisible(Long userId, List<String> roleCodes) {
|
||||||
return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getNotifyUsers());
|
return isPermissionControlledFeatureVisible(
|
||||||
|
userId,
|
||||||
|
DAILY_REPORT_NOTIFY_USERS_PERMISSION,
|
||||||
|
roleCodes,
|
||||||
|
workReportProperties.getDailyReport().getNotifyUsers());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDailyReportAttachmentsVisible(List<String> roleCodes) {
|
private boolean isDailyReportAttachmentsVisible(Long userId, List<String> roleCodes) {
|
||||||
return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getAttachments());
|
return isPermissionControlledFeatureVisible(
|
||||||
|
userId,
|
||||||
|
DAILY_REPORT_ATTACHMENTS_PERMISSION,
|
||||||
|
roleCodes,
|
||||||
|
workReportProperties.getDailyReport().getAttachments());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSalesRole(List<String> roleCodes) {
|
||||||
|
return roleCodes != null && roleCodes.stream().anyMatch(SALES_ROLE_CODE::equals);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFeatureVisible(List<String> roleCodes, WorkReportProperties.FeatureVisibility visibility) {
|
private boolean isFeatureVisible(List<String> roleCodes, WorkReportProperties.FeatureVisibility visibility) {
|
||||||
|
|
@ -549,6 +615,45 @@ public class WorkServiceImpl implements WorkService {
|
||||||
return safeVisibility.isVisibleByDefault();
|
return safeVisibility.isVisibleByDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isPermissionControlledFeatureVisible(
|
||||||
|
Long userId,
|
||||||
|
String permissionCode,
|
||||||
|
List<String> roleCodes,
|
||||||
|
WorkReportProperties.FeatureVisibility fallbackVisibility) {
|
||||||
|
if (!permissionCodeExists(permissionCode)) {
|
||||||
|
return isFeatureVisible(roleCodes, fallbackVisibility);
|
||||||
|
}
|
||||||
|
return loadPermissionCodes(userId).contains(permissionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> loadPermissionCodes(Long userId) {
|
||||||
|
try {
|
||||||
|
Long tenantId = resolveCurrentTenantId();
|
||||||
|
if (tenantId == null || sysPermissionService == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
Set<String> permissionCodes = sysPermissionService.listPermissionCodesByUserId(userId, tenantId);
|
||||||
|
return permissionCodes == null ? Set.of() : permissionCodes;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean permissionCodeExists(String permissionCode) {
|
||||||
|
try {
|
||||||
|
if (jdbcTemplate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
|
"select count(1) from sys_permission where code = ? and coalesce(is_deleted, 0) = 0",
|
||||||
|
Integer.class,
|
||||||
|
permissionCode);
|
||||||
|
return count != null && count > 0;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean matchesConfiguredRole(List<String> userRoleCodes, List<String> configuredRoleCodes) {
|
private boolean matchesConfiguredRole(List<String> userRoleCodes, List<String> configuredRoleCodes) {
|
||||||
if (userRoleCodes == null || userRoleCodes.isEmpty() || configuredRoleCodes == null || configuredRoleCodes.isEmpty()) {
|
if (userRoleCodes == null || userRoleCodes.isEmpty() || configuredRoleCodes == null || configuredRoleCodes.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -568,18 +673,132 @@ public class WorkServiceImpl implements WorkService {
|
||||||
return normalized == null ? null : normalized.toLowerCase();
|
return normalized == null ? null : normalized.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WorkHistoryItemDTO> loadHistoryItems(HistoryType historyType, int fetchLimit) {
|
private List<WorkHistoryItemDTO> loadHistoryItems(Long userId, HistoryType historyType, int fetchLimit) {
|
||||||
List<WorkHistoryItemDTO> historyItems = new ArrayList<>();
|
List<WorkHistoryItemDTO> historyItems = new ArrayList<>();
|
||||||
if (historyType == null || historyType == HistoryType.CHECK_IN) {
|
if (historyType == null || historyType == HistoryType.CHECK_IN) {
|
||||||
historyItems.addAll(workMapper.selectCheckInHistory(fetchLimit));
|
historyItems.addAll(workMapper.selectCheckInHistory(fetchLimit));
|
||||||
|
List<Long> visibleUserIds = resolveExtraVisibleUserIds(userId, CrmDataVisibilityService.RESOURCE_CHECKIN);
|
||||||
|
if (!visibleUserIds.isEmpty()) {
|
||||||
|
historyItems.addAll(workMapper.selectCheckInHistoryByUserIds(visibleUserIds, fetchLimit));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (historyType == null || historyType == HistoryType.REPORT) {
|
if (historyType == null || historyType == HistoryType.REPORT) {
|
||||||
historyItems.addAll(workMapper.selectReportHistory(fetchLimit));
|
historyItems.addAll(workMapper.selectReportHistory(fetchLimit));
|
||||||
|
List<Long> visibleUserIds = resolveExtraVisibleUserIds(userId, CrmDataVisibilityService.RESOURCE_DAILY_REPORT);
|
||||||
|
if (!visibleUserIds.isEmpty()) {
|
||||||
|
historyItems.addAll(workMapper.selectReportHistoryByUserIds(visibleUserIds, fetchLimit));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
historyItems = deduplicateHistoryItems(historyItems);
|
||||||
historyItems.sort(buildHistoryComparator());
|
historyItems.sort(buildHistoryComparator());
|
||||||
return historyItems;
|
return historyItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Long> resolveExtraVisibleUserIds(Long userId, String resourceType) {
|
||||||
|
Long currentUserId = userId;
|
||||||
|
if (currentUserId == null || currentUserId <= 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Long tenantId = tenantProvider == null ? null : tenantProvider.getCurrentTenantId();
|
||||||
|
if (tenantId == null || tenantId <= 0 || crmDataVisibilityService == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
DataVisibility visibility = crmDataVisibilityService.resolveVisibility(currentUserId, tenantId, resourceType);
|
||||||
|
if (visibility.allDataAccess()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return normalizeVisibleUserIds(visibility.visibleOwnerUserIds(), currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> normalizeVisibleUserIds(List<Long> userIds, Long currentUserId) {
|
||||||
|
if (userIds == null || userIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Set<Long> normalized = new LinkedHashSet<>();
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
if (userId != null && userId > 0 && !userId.equals(currentUserId)) {
|
||||||
|
normalized.add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WorkHistoryItemDTO> deduplicateHistoryItems(List<WorkHistoryItemDTO> items) {
|
||||||
|
Map<String, WorkHistoryItemDTO> byKey = new LinkedHashMap<>();
|
||||||
|
for (WorkHistoryItemDTO item : items) {
|
||||||
|
if (item != null) {
|
||||||
|
byKey.putIfAbsent(item.getType() + ":" + item.getId(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(byKey.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WorkCheckInExportDTO> limitMergedWorkCheckInExportRows(
|
||||||
|
List<WorkCheckInExportDTO> scopedRows,
|
||||||
|
List<WorkCheckInExportDTO> extraRows) {
|
||||||
|
Map<String, WorkCheckInExportDTO> byKey = new LinkedHashMap<>();
|
||||||
|
for (WorkCheckInExportDTO row : scopedRows) {
|
||||||
|
if (row != null) {
|
||||||
|
byKey.putIfAbsent(buildCheckInExportRowKey(row), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (WorkCheckInExportDTO row : extraRows) {
|
||||||
|
if (row != null) {
|
||||||
|
byKey.putIfAbsent(buildCheckInExportRowKey(row), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byKey.values().stream()
|
||||||
|
.sorted(buildCheckInExportComparator())
|
||||||
|
.limit(EXPORT_LIMIT)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WorkDailyReportExportDTO> limitMergedWorkDailyReportExportRows(
|
||||||
|
List<WorkDailyReportExportDTO> scopedRows,
|
||||||
|
List<WorkDailyReportExportDTO> extraRows) {
|
||||||
|
Map<String, WorkDailyReportExportDTO> byKey = new LinkedHashMap<>();
|
||||||
|
for (WorkDailyReportExportDTO row : scopedRows) {
|
||||||
|
if (row != null) {
|
||||||
|
byKey.putIfAbsent(buildDailyReportExportRowKey(row), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (WorkDailyReportExportDTO row : extraRows) {
|
||||||
|
if (row != null) {
|
||||||
|
byKey.putIfAbsent(buildDailyReportExportRowKey(row), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byKey.values().stream()
|
||||||
|
.sorted(buildDailyReportExportComparator())
|
||||||
|
.limit(EXPORT_LIMIT)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<WorkCheckInExportDTO> buildCheckInExportComparator() {
|
||||||
|
return Comparator
|
||||||
|
.comparing(WorkCheckInExportDTO::getSortTime, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||||
|
.thenComparing(WorkCheckInExportDTO::getId, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<WorkDailyReportExportDTO> buildDailyReportExportComparator() {
|
||||||
|
return Comparator
|
||||||
|
.comparing(WorkDailyReportExportDTO::getSortTime, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||||
|
.thenComparing(WorkDailyReportExportDTO::getId, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCheckInExportRowKey(WorkCheckInExportDTO row) {
|
||||||
|
if (row.getId() != null) {
|
||||||
|
return "checkin:" + row.getId();
|
||||||
|
}
|
||||||
|
return "checkin:fallback:" + row.getCheckinDate() + "|" + row.getCheckinTime() + "|" + row.getUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDailyReportExportRowKey(WorkDailyReportExportDTO row) {
|
||||||
|
if (row.getId() != null) {
|
||||||
|
return "daily-report:" + row.getId();
|
||||||
|
}
|
||||||
|
return "daily-report:fallback:" + row.getReportDate() + "|" + row.getSubmitTime() + "|" + row.getUserName();
|
||||||
|
}
|
||||||
|
|
||||||
private HistoryType mapHistoryType(String type) {
|
private HistoryType mapHistoryType(String type) {
|
||||||
String normalizedType = normalizeOptionalText(type);
|
String normalizedType = normalizeOptionalText(type);
|
||||||
if (normalizedType == null) {
|
if (normalizedType == null) {
|
||||||
|
|
@ -1456,7 +1675,8 @@ public class WorkServiceImpl implements WorkService {
|
||||||
Long reportId,
|
Long reportId,
|
||||||
List<WorkReportLineItemDTO> previousLineItems,
|
List<WorkReportLineItemDTO> previousLineItems,
|
||||||
CreateWorkDailyReportRequest request,
|
CreateWorkDailyReportRequest request,
|
||||||
WorkDailyReportDTO currentReport) {
|
WorkDailyReportDTO currentReport,
|
||||||
|
boolean syncOpportunitySnapshot) {
|
||||||
LocalDate reportDate = resolveReportDate(currentReport);
|
LocalDate reportDate = resolveReportDate(currentReport);
|
||||||
|
|
||||||
for (WorkReportLineItemDTO item : previousLineItems) {
|
for (WorkReportLineItemDTO item : previousLineItems) {
|
||||||
|
|
@ -1498,11 +1718,14 @@ public class WorkServiceImpl implements WorkService {
|
||||||
WORK_REPORT_FOLLOW_UP_TYPE,
|
WORK_REPORT_FOLLOW_UP_TYPE,
|
||||||
appendReportAttachmentMetadata(item.getContent(), item.getAttachments()),
|
appendReportAttachmentMetadata(item.getContent(), item.getAttachments()),
|
||||||
resolveOpportunityNextAction(item));
|
resolveOpportunityNextAction(item));
|
||||||
workMapper.updateOpportunitySnapshot(
|
if (syncOpportunitySnapshot) {
|
||||||
item.getBizId(),
|
workMapper.updateOpportunitySnapshot(
|
||||||
normalizeSnapshotText(item.getLatestProgress()),
|
item.getBizId(),
|
||||||
normalizeSnapshotText(item.getNextPlan()),
|
normalizeSnapshotText(item.getLatestProgress()),
|
||||||
item.getStage());
|
normalizeSnapshotText(item.getNextPlan()),
|
||||||
|
item.getStage(),
|
||||||
|
userId);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
workMapper.deleteDailyReportExpansionFollowUps(
|
workMapper.deleteDailyReportExpansionFollowUps(
|
||||||
|
|
|
||||||
|
|
@ -89,17 +89,3 @@ unisbase:
|
||||||
required-role-codes: [crm_xs]
|
required-role-codes: [crm_xs]
|
||||||
# 允许不使用 #,可直接填写文字日报的角色编码。
|
# 允许不使用 #,可直接填写文字日报的角色编码。
|
||||||
optional-role-codes: []
|
optional-role-codes: []
|
||||||
notify-users:
|
|
||||||
# 未命中下方任何角色配置时,是否默认显示“选择通知用户”按钮。
|
|
||||||
visible-by-default: true
|
|
||||||
# 明确显示“选择通知用户”按钮的角色编码。
|
|
||||||
visible-role-codes: []
|
|
||||||
# 明确隐藏“选择通知用户”按钮的角色编码;命中后优先级高于 visible-role-codes。
|
|
||||||
hidden-role-codes: []
|
|
||||||
attachments:
|
|
||||||
# 未命中下方任何角色配置时,是否默认显示日报附件功能。
|
|
||||||
visible-by-default: true
|
|
||||||
# 明确显示日报附件功能的角色编码。
|
|
||||||
visible-role-codes: []
|
|
||||||
# 明确隐藏日报附件功能的角色编码;命中后优先级高于 visible-role-codes。
|
|
||||||
hidden-role-codes: [crm_xs]
|
|
||||||
|
|
|
||||||
|
|
@ -98,17 +98,3 @@ unisbase:
|
||||||
required-role-codes: [crm_xs]
|
required-role-codes: [crm_xs]
|
||||||
# 允许不使用 #,可直接填写文字日报的角色编码。
|
# 允许不使用 #,可直接填写文字日报的角色编码。
|
||||||
optional-role-codes: []
|
optional-role-codes: []
|
||||||
notify-users:
|
|
||||||
# 未命中下方任何角色配置时,是否默认显示“选择通知用户”按钮。
|
|
||||||
visible-by-default: true
|
|
||||||
# 明确显示“选择通知用户”按钮的角色编码。
|
|
||||||
visible-role-codes: []
|
|
||||||
# 明确隐藏“选择通知用户”按钮的角色编码;命中后优先级高于 visible-role-codes。
|
|
||||||
hidden-role-codes: []
|
|
||||||
attachments:
|
|
||||||
# 未命中下方任何角色配置时,是否默认显示日报附件功能。
|
|
||||||
visible-by-default: true
|
|
||||||
# 明确显示日报附件功能的角色编码。
|
|
||||||
visible-role-codes: []
|
|
||||||
# 明确隐藏日报附件功能的角色编码;命中后优先级高于 visible-role-codes。
|
|
||||||
hidden-role-codes: [crm_xs]
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,231 @@
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.unis.crm.mapper.ExpansionMapper">
|
<mapper namespace="com.unis.crm.mapper.ExpansionMapper">
|
||||||
|
|
||||||
|
<sql id="salesExpansionSelectColumns">
|
||||||
|
s.id,
|
||||||
|
s.owner_user_id as ownerUserId,
|
||||||
|
coalesce(nullif(u.display_name, ''), nullif(u.username, ''), '无') as owner,
|
||||||
|
'sales' as type,
|
||||||
|
coalesce(s.employee_no, '无') as employeeNo,
|
||||||
|
s.candidate_name as name,
|
||||||
|
coalesce(office_dict.item_value, s.office_name, '') as officeCode,
|
||||||
|
coalesce(office_dict.item_label, nullif(s.office_name, ''), '无') as officeName,
|
||||||
|
coalesce(s.mobile, '无') as phone,
|
||||||
|
coalesce(s.email, '无') as email,
|
||||||
|
coalesce(s.target_dept, '') as targetDept,
|
||||||
|
coalesce(nullif(s.target_dept, ''), '无') as dept,
|
||||||
|
coalesce(industry_dict.item_value, s.industry, '') as industryCode,
|
||||||
|
coalesce(industry_dict.item_label, nullif(s.industry, ''), '无') as industry,
|
||||||
|
coalesce(s.title, '无') as title,
|
||||||
|
s.intent_level as intentLevel,
|
||||||
|
case s.intent_level
|
||||||
|
when 'high' then '高'
|
||||||
|
when 'medium' then '中'
|
||||||
|
when 'low' then '低'
|
||||||
|
else '无'
|
||||||
|
end as intent,
|
||||||
|
s.stage as stageCode,
|
||||||
|
case s.stage
|
||||||
|
when 'initial_contact' then '初步沟通'
|
||||||
|
when 'solution_discussion' then '方案交流'
|
||||||
|
when 'bidding' then '招投标'
|
||||||
|
when 'business_negotiation' then '商务谈判'
|
||||||
|
when 'won' then '已成交'
|
||||||
|
when 'lost' then '已放弃'
|
||||||
|
else coalesce(s.stage, '无')
|
||||||
|
end as stage,
|
||||||
|
s.has_desktop_exp as hasExp,
|
||||||
|
s.in_progress as inProgress,
|
||||||
|
(s.employment_status = 'active') as active,
|
||||||
|
s.employment_status as employmentStatus,
|
||||||
|
coalesce(to_char(s.expected_join_date, 'YYYY-MM-DD'), '无') as expectedJoinDate,
|
||||||
|
coalesce(to_char(s.created_at, 'YYYY-MM-DD HH24:MI'), '无') as createdAt,
|
||||||
|
coalesce(
|
||||||
|
to_char(
|
||||||
|
case
|
||||||
|
when sales_followup.latest_followup_time is null then s.updated_at
|
||||||
|
else greatest(s.updated_at, sales_followup.latest_followup_time)
|
||||||
|
end,
|
||||||
|
'YYYY-MM-DD HH24:MI'
|
||||||
|
),
|
||||||
|
'无'
|
||||||
|
) as updatedAt,
|
||||||
|
coalesce(s.remark, '无') as notes
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="salesExpansionJoins">
|
||||||
|
left join (
|
||||||
|
select
|
||||||
|
f.biz_id,
|
||||||
|
max(f.followup_time) as latest_followup_time
|
||||||
|
from crm_expansion_followup f
|
||||||
|
where f.biz_type = 'sales'
|
||||||
|
group by f.biz_id
|
||||||
|
) sales_followup on sales_followup.biz_id = s.id
|
||||||
|
left join sys_user u
|
||||||
|
on u.user_id = s.owner_user_id
|
||||||
|
and u.is_deleted = 0
|
||||||
|
left join sys_dict_item office_dict
|
||||||
|
on office_dict.type_code = 'tz_bsc'
|
||||||
|
and office_dict.item_value = s.office_name
|
||||||
|
and office_dict.status = 1
|
||||||
|
and coalesce(office_dict.is_deleted, 0) = 0
|
||||||
|
left join sys_dict_item industry_dict
|
||||||
|
on industry_dict.type_code = 'tz_sshy'
|
||||||
|
and industry_dict.item_value = s.industry
|
||||||
|
and industry_dict.status = 1
|
||||||
|
and coalesce(industry_dict.is_deleted, 0) = 0
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="salesExpansionKeywordFilter">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(s.employee_no, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or s.candidate_name ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(office_dict.item_label, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.target_dept, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(industry_dict.item_label, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="channelExpansionSelectColumns">
|
||||||
|
c.id,
|
||||||
|
c.owner_user_id as ownerUserId,
|
||||||
|
coalesce(nullif(u.display_name, ''), nullif(u.username, ''), '无') as owner,
|
||||||
|
'channel' as type,
|
||||||
|
coalesce(c.channel_code, '') as channelCode,
|
||||||
|
c.channel_name as name,
|
||||||
|
coalesce(c.province, '') as provinceCode,
|
||||||
|
coalesce(province_area.name, nullif(c.province, ''), '无') as province,
|
||||||
|
coalesce(c.city, '') as cityCode,
|
||||||
|
coalesce(city_area.name, nullif(c.city, ''), '无') as city,
|
||||||
|
coalesce(c.office_address, '无') as officeAddress,
|
||||||
|
coalesce(c.channel_industry, c.industry, '') as channelIndustryCode,
|
||||||
|
coalesce(c.channel_industry, c.industry, '无') as channelIndustry,
|
||||||
|
coalesce(c.certification_level, '无') as certificationLevel,
|
||||||
|
coalesce(trim(to_char(c.annual_revenue, 'FM999999990.##')), '') as annualRevenue,
|
||||||
|
case
|
||||||
|
when c.annual_revenue is null then '无'
|
||||||
|
else trim(to_char(c.annual_revenue, 'FM999999990.##')) || '万元'
|
||||||
|
end as revenue,
|
||||||
|
coalesce(c.staff_size, 0) as size,
|
||||||
|
coalesce(primary_contact.contact_name, c.contact_name, '无') as primaryContactName,
|
||||||
|
coalesce(primary_contact.contact_title, c.contact_title, '无') as primaryContactTitle,
|
||||||
|
coalesce(primary_contact.contact_mobile, c.contact_mobile, '无') as primaryContactMobile,
|
||||||
|
coalesce(to_char(c.contact_established_date, 'YYYY-MM-DD'), '无') as establishedDate,
|
||||||
|
c.intent_level as intentLevel,
|
||||||
|
case c.intent_level
|
||||||
|
when 'high' then '高'
|
||||||
|
when 'medium' then '中'
|
||||||
|
when 'low' then '低'
|
||||||
|
else '无'
|
||||||
|
end as intent,
|
||||||
|
coalesce(c.has_desktop_exp, false) as hasDesktopExp,
|
||||||
|
coalesce(c.channel_attribute, '') as channelAttributeCode,
|
||||||
|
coalesce(channel_attribute_dict.item_label, c.channel_attribute, '无') as channelAttribute,
|
||||||
|
coalesce(c.internal_attribute, '') as internalAttributeCode,
|
||||||
|
coalesce(internal_attribute_dict.item_label, c.internal_attribute, '无') as internalAttribute,
|
||||||
|
c.stage as stageCode,
|
||||||
|
case c.stage
|
||||||
|
when 'initial_contact' then '初步接触'
|
||||||
|
when 'solution_discussion' then '方案交流'
|
||||||
|
when 'bidding' then '招投标'
|
||||||
|
when 'business_negotiation' then '合作洽谈'
|
||||||
|
when 'won' then '已合作'
|
||||||
|
when 'lost' then '已终止'
|
||||||
|
else coalesce(c.stage, '无')
|
||||||
|
end as stage,
|
||||||
|
c.landed_flag as landed,
|
||||||
|
coalesce(to_char(c.expected_sign_date, 'YYYY-MM-DD'), '无') as expectedSignDate,
|
||||||
|
coalesce(to_char(c.created_at, 'YYYY-MM-DD HH24:MI'), '无') as createdAt,
|
||||||
|
coalesce(
|
||||||
|
to_char(
|
||||||
|
case
|
||||||
|
when channel_followup.latest_followup_time is null then c.updated_at
|
||||||
|
else greatest(c.updated_at, channel_followup.latest_followup_time)
|
||||||
|
end,
|
||||||
|
'YYYY-MM-DD HH24:MI'
|
||||||
|
),
|
||||||
|
'无'
|
||||||
|
) as updatedAt,
|
||||||
|
coalesce(c.remark, '无') as notes
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="channelExpansionJoins">
|
||||||
|
left join (
|
||||||
|
select
|
||||||
|
f.biz_id,
|
||||||
|
max(f.followup_time) as latest_followup_time
|
||||||
|
from crm_expansion_followup f
|
||||||
|
where f.biz_type = 'channel'
|
||||||
|
group by f.biz_id
|
||||||
|
) channel_followup on channel_followup.biz_id = c.id
|
||||||
|
left join sys_user u
|
||||||
|
on u.user_id = c.owner_user_id
|
||||||
|
and u.is_deleted = 0
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
contact_name,
|
||||||
|
contact_title,
|
||||||
|
contact_mobile
|
||||||
|
from crm_channel_expansion_contact cc
|
||||||
|
where cc.channel_expansion_id = c.id
|
||||||
|
order by cc.sort_order asc nulls last, cc.id asc
|
||||||
|
limit 1
|
||||||
|
) primary_contact on true
|
||||||
|
left join cnarea province_area
|
||||||
|
on province_area.level = 1
|
||||||
|
and (province_area.name = c.province or province_area.area_code = c.province)
|
||||||
|
left join cnarea city_area
|
||||||
|
on city_area.level = 2
|
||||||
|
and (city_area.name = c.city or city_area.area_code = c.city)
|
||||||
|
left join sys_dict_item channel_attribute_dict
|
||||||
|
on channel_attribute_dict.type_code = 'tz_qdsx'
|
||||||
|
and channel_attribute_dict.item_value = c.channel_attribute
|
||||||
|
and channel_attribute_dict.status = 1
|
||||||
|
and coalesce(channel_attribute_dict.is_deleted, 0) = 0
|
||||||
|
left join sys_dict_item internal_attribute_dict
|
||||||
|
on internal_attribute_dict.type_code = 'tz_xhsnbsx'
|
||||||
|
and internal_attribute_dict.item_value = c.internal_attribute
|
||||||
|
and internal_attribute_dict.status = 1
|
||||||
|
and coalesce(internal_attribute_dict.is_deleted, 0) = 0
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="channelExpansionKeywordFilter">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
c.channel_name ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.channel_code, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.channel_industry, c.industry, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.province, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.city, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.certification_level, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(province_area.name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(city_area.name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from sys_dict_item industry_dict
|
||||||
|
where industry_dict.type_code = 'tz_sshy'
|
||||||
|
and industry_dict.status = 1
|
||||||
|
and coalesce(industry_dict.is_deleted, 0) = 0
|
||||||
|
and industry_dict.item_value = any(string_to_array(coalesce(c.channel_industry, c.industry, ''), ','))
|
||||||
|
and industry_dict.item_label ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from crm_channel_expansion_contact cc
|
||||||
|
where cc.channel_expansion_id = c.id
|
||||||
|
and (
|
||||||
|
coalesce(cc.contact_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(cc.contact_mobile, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(cc.contact_title, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
<select id="selectDictItems" resultType="com.unis.crm.dto.expansion.DictOptionDTO">
|
<select id="selectDictItems" resultType="com.unis.crm.dto.expansion.DictOptionDTO">
|
||||||
select
|
select
|
||||||
item_label as label,
|
item_label as label,
|
||||||
|
|
@ -136,6 +361,19 @@
|
||||||
order by s.updated_at desc, s.id desc
|
order by s.updated_at desc, s.id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectSalesExpansionsByOwnerUserIds" resultType="com.unis.crm.dto.expansion.SalesExpansionItemDTO">
|
||||||
|
select
|
||||||
|
<include refid="salesExpansionSelectColumns"/>
|
||||||
|
from crm_sales_expansion s
|
||||||
|
<include refid="salesExpansionJoins"/>
|
||||||
|
where s.owner_user_id in
|
||||||
|
<foreach collection="ownerUserIds" item="ownerUserId" open="(" separator="," close=")">
|
||||||
|
#{ownerUserId}
|
||||||
|
</foreach>
|
||||||
|
<include refid="salesExpansionKeywordFilter"/>
|
||||||
|
order by s.updated_at desc, s.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectChannelExpansions" resultType="com.unis.crm.dto.expansion.ChannelExpansionItemDTO">
|
<select id="selectChannelExpansions" resultType="com.unis.crm.dto.expansion.ChannelExpansionItemDTO">
|
||||||
select
|
select
|
||||||
c.id,
|
c.id,
|
||||||
|
|
@ -271,6 +509,19 @@
|
||||||
order by c.updated_at desc, c.id desc
|
order by c.updated_at desc, c.id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChannelExpansionsByOwnerUserIds" resultType="com.unis.crm.dto.expansion.ChannelExpansionItemDTO">
|
||||||
|
select
|
||||||
|
<include refid="channelExpansionSelectColumns"/>
|
||||||
|
from crm_channel_expansion c
|
||||||
|
<include refid="channelExpansionJoins"/>
|
||||||
|
where c.owner_user_id in
|
||||||
|
<foreach collection="ownerUserIds" item="ownerUserId" open="(" separator="," close=")">
|
||||||
|
#{ownerUserId}
|
||||||
|
</foreach>
|
||||||
|
<include refid="channelExpansionKeywordFilter"/>
|
||||||
|
order by c.updated_at desc, c.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectSalesExpansionsForTenant" resultType="com.unis.crm.dto.expansion.SalesExpansionItemDTO">
|
<select id="selectSalesExpansionsForTenant" resultType="com.unis.crm.dto.expansion.SalesExpansionItemDTO">
|
||||||
select
|
select
|
||||||
s.id,
|
s.id,
|
||||||
|
|
|
||||||
|
|
@ -377,10 +377,21 @@
|
||||||
coalesce(f.followup_type, '无') as type,
|
coalesce(f.followup_type, '无') as type,
|
||||||
coalesce(f.content, '无') as content,
|
coalesce(f.content, '无') as content,
|
||||||
coalesce(f.next_action, '') as nextAction,
|
coalesce(f.next_action, '') as nextAction,
|
||||||
coalesce(u.display_name, '无') as user
|
coalesce(u.display_name, '无') as user,
|
||||||
|
coalesce(role_info.role_codes, '') as roleCodes
|
||||||
from crm_opportunity_followup f
|
from crm_opportunity_followup f
|
||||||
join crm_opportunity o on o.id = f.opportunity_id
|
join crm_opportunity o on o.id = f.opportunity_id
|
||||||
left join sys_user u on u.user_id = f.followup_user_id
|
left join sys_user u on u.user_id = f.followup_user_id
|
||||||
|
left join (
|
||||||
|
select
|
||||||
|
ur.user_id,
|
||||||
|
string_agg(distinct r.role_code, ',' order by r.role_code) as role_codes
|
||||||
|
from sys_user_role ur
|
||||||
|
join sys_role r on r.role_id = ur.role_id
|
||||||
|
where coalesce(ur.is_deleted, 0) = 0
|
||||||
|
and coalesce(r.is_deleted, 0) = 0
|
||||||
|
group by ur.user_id
|
||||||
|
) role_info on role_info.user_id = f.followup_user_id
|
||||||
where f.opportunity_id in
|
where f.opportunity_id in
|
||||||
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
|
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
|
||||||
#{id}
|
#{id}
|
||||||
|
|
@ -448,6 +459,7 @@
|
||||||
competitor_name,
|
competitor_name,
|
||||||
latest_progress,
|
latest_progress,
|
||||||
next_plan,
|
next_plan,
|
||||||
|
updated_by,
|
||||||
pushed_to_oms,
|
pushed_to_oms,
|
||||||
oms_push_time,
|
oms_push_time,
|
||||||
description,
|
description,
|
||||||
|
|
@ -473,6 +485,7 @@
|
||||||
#{request.competitorName},
|
#{request.competitorName},
|
||||||
#{request.latestProgress},
|
#{request.latestProgress},
|
||||||
#{request.nextPlan},
|
#{request.nextPlan},
|
||||||
|
#{userId},
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
#{request.description},
|
#{request.description},
|
||||||
|
|
@ -509,6 +522,7 @@
|
||||||
<update id="updateOpportunityCode">
|
<update id="updateOpportunityCode">
|
||||||
update crm_opportunity o
|
update crm_opportunity o
|
||||||
set opportunity_code = #{opportunityCode},
|
set opportunity_code = #{opportunityCode},
|
||||||
|
updated_by = #{userId},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where o.id = #{opportunityId}
|
where o.id = #{opportunityId}
|
||||||
</update>
|
</update>
|
||||||
|
|
@ -575,6 +589,7 @@
|
||||||
update crm_opportunity o
|
update crm_opportunity o
|
||||||
set pre_sales_id = #{preSalesId},
|
set pre_sales_id = #{preSalesId},
|
||||||
pre_sales_name = #{preSalesName},
|
pre_sales_name = #{preSalesName},
|
||||||
|
updated_by = #{userId},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where o.id = #{opportunityId}
|
where o.id = #{opportunityId}
|
||||||
</update>
|
</update>
|
||||||
|
|
@ -597,6 +612,7 @@
|
||||||
competitor_name = #{request.competitorName},
|
competitor_name = #{request.competitorName},
|
||||||
latest_progress = #{request.latestProgress},
|
latest_progress = #{request.latestProgress},
|
||||||
next_plan = #{request.nextPlan},
|
next_plan = #{request.nextPlan},
|
||||||
|
updated_by = #{userId},
|
||||||
description = #{request.description},
|
description = #{request.description},
|
||||||
status = case
|
status = case
|
||||||
when #{request.stage} = 'won' then 'won'
|
when #{request.stage} = 'won' then 'won'
|
||||||
|
|
@ -711,6 +727,7 @@
|
||||||
set pushed_to_oms = true,
|
set pushed_to_oms = true,
|
||||||
oms_push_time = now(),
|
oms_push_time = now(),
|
||||||
opportunity_code = #{opportunityCode},
|
opportunity_code = #{opportunityCode},
|
||||||
|
updated_by = #{userId},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where o.id = #{opportunityId}
|
where o.id = #{opportunityId}
|
||||||
</update>
|
</update>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,58 @@
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.unis.crm.mapper.WorkMapper">
|
<mapper namespace="com.unis.crm.mapper.WorkMapper">
|
||||||
|
|
||||||
|
<sql id="checkInExportFilters">
|
||||||
|
<if test="startDate != null">
|
||||||
|
and c.checkin_date >= #{startDate}
|
||||||
|
</if>
|
||||||
|
<if test="endDate != null">
|
||||||
|
and c.checkin_date <= #{endDate}
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(c.user_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.biz_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.location_text, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.remark, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="deptName != null and deptName != ''">
|
||||||
|
and coalesce(nullif(btrim(c.dept_name), ''), nullif(btrim(org_info.org_names), ''), '') ilike concat('%', #{deptName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bizType != null and bizType != ''">
|
||||||
|
and c.biz_type = #{bizType}
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">
|
||||||
|
and coalesce(c.status, 'normal') = #{status}
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="dailyReportExportFilters">
|
||||||
|
<if test="startDate != null">
|
||||||
|
and r.report_date >= #{startDate}
|
||||||
|
</if>
|
||||||
|
<if test="endDate != null">
|
||||||
|
and r.report_date <= #{endDate}
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(r.work_content, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(r.tomorrow_plan, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(rc.comment_content, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="deptName != null and deptName != ''">
|
||||||
|
and coalesce(nullif(btrim(org_info.org_names), ''), '') ilike concat('%', #{deptName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">
|
||||||
|
and coalesce(r.status, 'submitted') = #{status}
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
<select id="selectTodayCheckIn" resultType="com.unis.crm.dto.work.WorkCheckInDTO">
|
<select id="selectTodayCheckIn" resultType="com.unis.crm.dto.work.WorkCheckInDTO">
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
|
|
@ -228,6 +280,42 @@
|
||||||
limit #{limit}
|
limit #{limit}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectCheckInHistoryByUserIds" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
'外勤打卡' as type,
|
||||||
|
to_char(c.checkin_date, 'YYYY-MM-DD') as date,
|
||||||
|
to_char(c.checkin_time, 'HH24:MI') as time,
|
||||||
|
case
|
||||||
|
when c.biz_name is not null and btrim(c.biz_name) <> '' then '关联对象:' || c.biz_name || E'\n'
|
||||||
|
else ''
|
||||||
|
end ||
|
||||||
|
case
|
||||||
|
when c.user_name is not null and btrim(c.user_name) <> '' then '打卡人:' || c.user_name || E'\n'
|
||||||
|
else ''
|
||||||
|
end ||
|
||||||
|
coalesce(c.location_text, '') ||
|
||||||
|
case
|
||||||
|
when c.remark is not null and btrim(c.remark) <> '' then E'\n备注:' || c.remark
|
||||||
|
else ''
|
||||||
|
end as content,
|
||||||
|
case coalesce(c.status, 'normal')
|
||||||
|
when 'normal' then '正常'
|
||||||
|
when 'updated' then '已更新'
|
||||||
|
else coalesce(c.status, '正常')
|
||||||
|
end as status,
|
||||||
|
null::integer as score,
|
||||||
|
null::text as comment,
|
||||||
|
coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) as sort_time
|
||||||
|
from work_checkin c
|
||||||
|
where c.user_id in
|
||||||
|
<foreach collection="visibleUserIds" item="userId" open="(" separator="," close=")">
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
order by sort_time desc nulls last, id desc
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectReportHistory" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
<select id="selectReportHistory" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
||||||
select
|
select
|
||||||
r.id,
|
r.id,
|
||||||
|
|
@ -270,6 +358,52 @@
|
||||||
limit #{limit}
|
limit #{limit}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportHistoryByUserIds" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
||||||
|
select
|
||||||
|
r.id,
|
||||||
|
'日报' as type,
|
||||||
|
to_char(r.report_date, 'YYYY-MM-DD') as date,
|
||||||
|
to_char(r.submit_time, 'HH24:MI') as time,
|
||||||
|
case
|
||||||
|
when coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), '')) is not null
|
||||||
|
then '提交人:' || coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), '')) || E'\n'
|
||||||
|
else ''
|
||||||
|
end ||
|
||||||
|
coalesce(r.work_content, '') ||
|
||||||
|
case
|
||||||
|
when r.tomorrow_plan is not null and btrim(r.tomorrow_plan) <> '' then E'\n明日计划:' || r.tomorrow_plan
|
||||||
|
else ''
|
||||||
|
end as content,
|
||||||
|
case coalesce(rc.comment_content, '')
|
||||||
|
when '' then
|
||||||
|
case coalesce(r.status, 'submitted')
|
||||||
|
when 'submitted' then '已提交'
|
||||||
|
when 'reviewed' then '已点评'
|
||||||
|
else coalesce(r.status, '已提交')
|
||||||
|
end
|
||||||
|
else '已点评'
|
||||||
|
end as status,
|
||||||
|
rc.score,
|
||||||
|
rc.comment_content as comment,
|
||||||
|
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sort_time
|
||||||
|
from work_daily_report r
|
||||||
|
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
||||||
|
left join (
|
||||||
|
select distinct on (report_id)
|
||||||
|
report_id,
|
||||||
|
score,
|
||||||
|
comment_content
|
||||||
|
from work_daily_report_comment
|
||||||
|
order by report_id, reviewed_at desc nulls last, id desc
|
||||||
|
) rc on rc.report_id = r.id
|
||||||
|
where r.user_id in
|
||||||
|
<foreach collection="visibleUserIds" item="userId" open="(" separator="," close=")">
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
order by sort_time desc nulls last, id desc
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectReportHistoryById" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
<select id="selectReportHistoryById" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
||||||
select
|
select
|
||||||
r.id,
|
r.id,
|
||||||
|
|
@ -321,8 +455,55 @@
|
||||||
limit 1
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportHistoryByIdForUserIds" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
|
||||||
|
select
|
||||||
|
r.id,
|
||||||
|
'日报' as type,
|
||||||
|
to_char(r.report_date, 'YYYY-MM-DD') as date,
|
||||||
|
to_char(r.submit_time, 'HH24:MI') as time,
|
||||||
|
case
|
||||||
|
when coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), '')) is not null
|
||||||
|
then '提交人:' || coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), '')) || E'\n'
|
||||||
|
else ''
|
||||||
|
end ||
|
||||||
|
coalesce(r.work_content, '') ||
|
||||||
|
case
|
||||||
|
when r.tomorrow_plan is not null and btrim(r.tomorrow_plan) <> '' then E'\n明日计划:' || r.tomorrow_plan
|
||||||
|
else ''
|
||||||
|
end as content,
|
||||||
|
case coalesce(rc.comment_content, '')
|
||||||
|
when '' then
|
||||||
|
case coalesce(r.status, 'submitted')
|
||||||
|
when 'submitted' then '已提交'
|
||||||
|
when 'reviewed' then '已点评'
|
||||||
|
else coalesce(r.status, '已提交')
|
||||||
|
end
|
||||||
|
else '已点评'
|
||||||
|
end as status,
|
||||||
|
rc.score,
|
||||||
|
rc.comment_content as comment,
|
||||||
|
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sort_time
|
||||||
|
from work_daily_report r
|
||||||
|
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
||||||
|
left join (
|
||||||
|
select distinct on (report_id)
|
||||||
|
report_id,
|
||||||
|
score,
|
||||||
|
comment_content
|
||||||
|
from work_daily_report_comment
|
||||||
|
order by report_id, reviewed_at desc nulls last, id desc
|
||||||
|
) rc on rc.report_id = r.id
|
||||||
|
where r.id = #{reportId}
|
||||||
|
and r.user_id in
|
||||||
|
<foreach collection="visibleUserIds" item="userId" open="(" separator="," close=")">
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectCheckInExportRows" resultType="com.unis.crm.dto.work.WorkCheckInExportDTO">
|
<select id="selectCheckInExportRows" resultType="com.unis.crm.dto.work.WorkCheckInExportDTO">
|
||||||
select
|
select
|
||||||
|
c.id,
|
||||||
to_char(c.checkin_date, 'YYYY-MM-DD') as checkinDate,
|
to_char(c.checkin_date, 'YYYY-MM-DD') as checkinDate,
|
||||||
to_char(c.checkin_time, 'HH24:MI') as checkinTime,
|
to_char(c.checkin_time, 'HH24:MI') as checkinTime,
|
||||||
coalesce(nullif(btrim(c.user_name), ''), nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
coalesce(nullif(btrim(c.user_name), ''), nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
||||||
|
|
@ -335,7 +516,8 @@
|
||||||
coalesce(c.remark, '') as remark,
|
coalesce(c.remark, '') as remark,
|
||||||
coalesce(c.status, 'normal') as status,
|
coalesce(c.status, 'normal') as status,
|
||||||
to_char(c.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
to_char(c.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
||||||
to_char(c.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt
|
to_char(c.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt,
|
||||||
|
coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) as sortTime
|
||||||
from work_checkin c
|
from work_checkin c
|
||||||
left join sys_user u on u.user_id = c.user_id and u.is_deleted = 0
|
left join sys_user u on u.user_id = c.user_id and u.is_deleted = 0
|
||||||
left join lateral (
|
left join lateral (
|
||||||
|
|
@ -347,37 +529,50 @@
|
||||||
and o.is_deleted = 0
|
and o.is_deleted = 0
|
||||||
) org_info on true
|
) org_info on true
|
||||||
where 1 = 1
|
where 1 = 1
|
||||||
<if test="startDate != null">
|
<include refid="checkInExportFilters"/>
|
||||||
and c.checkin_date >= #{startDate}
|
order by coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) desc nulls last, c.id desc
|
||||||
</if>
|
limit #{limit}
|
||||||
<if test="endDate != null">
|
</select>
|
||||||
and c.checkin_date <= #{endDate}
|
|
||||||
</if>
|
<select id="selectCheckInExportRowsByUserIds" resultType="com.unis.crm.dto.work.WorkCheckInExportDTO">
|
||||||
<if test="keyword != null and keyword != ''">
|
select
|
||||||
and (
|
c.id,
|
||||||
coalesce(c.user_name, '') ilike concat('%', #{keyword}, '%')
|
to_char(c.checkin_date, 'YYYY-MM-DD') as checkinDate,
|
||||||
or coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
to_char(c.checkin_time, 'HH24:MI') as checkinTime,
|
||||||
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
coalesce(nullif(btrim(c.user_name), ''), nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
||||||
or coalesce(c.biz_name, '') ilike concat('%', #{keyword}, '%')
|
coalesce(nullif(btrim(c.dept_name), ''), nullif(btrim(org_info.org_names), ''), '') as deptName,
|
||||||
or coalesce(c.location_text, '') ilike concat('%', #{keyword}, '%')
|
coalesce(c.biz_type, '') as bizType,
|
||||||
or coalesce(c.remark, '') ilike concat('%', #{keyword}, '%')
|
coalesce(c.biz_name, '') as bizName,
|
||||||
)
|
coalesce(c.location_text, '') as locationText,
|
||||||
</if>
|
c.longitude,
|
||||||
<if test="deptName != null and deptName != ''">
|
c.latitude,
|
||||||
and coalesce(nullif(btrim(c.dept_name), ''), nullif(btrim(org_info.org_names), ''), '') ilike concat('%', #{deptName}, '%')
|
coalesce(c.remark, '') as remark,
|
||||||
</if>
|
coalesce(c.status, 'normal') as status,
|
||||||
<if test="bizType != null and bizType != ''">
|
to_char(c.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
||||||
and c.biz_type = #{bizType}
|
to_char(c.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt,
|
||||||
</if>
|
coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) as sortTime
|
||||||
<if test="status != null and status != ''">
|
from work_checkin c
|
||||||
and coalesce(c.status, 'normal') = #{status}
|
left join sys_user u on u.user_id = c.user_id and u.is_deleted = 0
|
||||||
</if>
|
left join lateral (
|
||||||
|
select string_agg(distinct o.org_name, '、' order by o.org_name) as org_names
|
||||||
|
from sys_tenant_user tu
|
||||||
|
join sys_org o on o.id = tu.org_id
|
||||||
|
where tu.user_id = c.user_id
|
||||||
|
and tu.is_deleted = 0
|
||||||
|
and o.is_deleted = 0
|
||||||
|
) org_info on true
|
||||||
|
where c.user_id in
|
||||||
|
<foreach collection="visibleUserIds" item="userId" open="(" separator="," close=")">
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
<include refid="checkInExportFilters"/>
|
||||||
order by coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) desc nulls last, c.id desc
|
order by coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) desc nulls last, c.id desc
|
||||||
limit #{limit}
|
limit #{limit}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectDailyReportExportRows" resultType="com.unis.crm.dto.work.WorkDailyReportExportDTO">
|
<select id="selectDailyReportExportRows" resultType="com.unis.crm.dto.work.WorkDailyReportExportDTO">
|
||||||
select
|
select
|
||||||
|
r.id,
|
||||||
to_char(r.report_date, 'YYYY-MM-DD') as reportDate,
|
to_char(r.report_date, 'YYYY-MM-DD') as reportDate,
|
||||||
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
|
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
|
||||||
coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
||||||
|
|
@ -391,7 +586,8 @@
|
||||||
coalesce(nullif(btrim(reviewer.display_name), ''), nullif(btrim(reviewer.username), ''), '') as reviewerName,
|
coalesce(nullif(btrim(reviewer.display_name), ''), nullif(btrim(reviewer.username), ''), '') as reviewerName,
|
||||||
to_char(rc.reviewed_at, 'YYYY-MM-DD HH24:MI') as reviewedAt,
|
to_char(rc.reviewed_at, 'YYYY-MM-DD HH24:MI') as reviewedAt,
|
||||||
to_char(r.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
to_char(r.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
||||||
to_char(r.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt
|
to_char(r.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt,
|
||||||
|
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sortTime
|
||||||
from work_daily_report r
|
from work_daily_report r
|
||||||
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
||||||
left join lateral (
|
left join lateral (
|
||||||
|
|
@ -414,27 +610,55 @@
|
||||||
) rc on rc.report_id = r.id
|
) rc on rc.report_id = r.id
|
||||||
left join sys_user reviewer on reviewer.user_id = rc.reviewer_user_id and reviewer.is_deleted = 0
|
left join sys_user reviewer on reviewer.user_id = rc.reviewer_user_id and reviewer.is_deleted = 0
|
||||||
where 1 = 1
|
where 1 = 1
|
||||||
<if test="startDate != null">
|
<include refid="dailyReportExportFilters"/>
|
||||||
and r.report_date >= #{startDate}
|
order by coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) desc nulls last, r.id desc
|
||||||
</if>
|
limit #{limit}
|
||||||
<if test="endDate != null">
|
</select>
|
||||||
and r.report_date <= #{endDate}
|
|
||||||
</if>
|
<select id="selectDailyReportExportRowsByUserIds" resultType="com.unis.crm.dto.work.WorkDailyReportExportDTO">
|
||||||
<if test="keyword != null and keyword != ''">
|
select
|
||||||
and (
|
r.id,
|
||||||
coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
to_char(r.report_date, 'YYYY-MM-DD') as reportDate,
|
||||||
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
|
||||||
or coalesce(r.work_content, '') ilike concat('%', #{keyword}, '%')
|
coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as userName,
|
||||||
or coalesce(r.tomorrow_plan, '') ilike concat('%', #{keyword}, '%')
|
coalesce(nullif(btrim(org_info.org_names), ''), '') as deptName,
|
||||||
or coalesce(rc.comment_content, '') ilike concat('%', #{keyword}, '%')
|
coalesce(r.source_type, 'manual') as sourceType,
|
||||||
)
|
coalesce(r.status, 'submitted') as status,
|
||||||
</if>
|
coalesce(r.work_content, '') as workContent,
|
||||||
<if test="deptName != null and deptName != ''">
|
coalesce(r.tomorrow_plan, '') as tomorrowPlan,
|
||||||
and coalesce(nullif(btrim(org_info.org_names), ''), '') ilike concat('%', #{deptName}, '%')
|
rc.score,
|
||||||
</if>
|
rc.comment_content as comment,
|
||||||
<if test="status != null and status != ''">
|
coalesce(nullif(btrim(reviewer.display_name), ''), nullif(btrim(reviewer.username), ''), '') as reviewerName,
|
||||||
and coalesce(r.status, 'submitted') = #{status}
|
to_char(rc.reviewed_at, 'YYYY-MM-DD HH24:MI') as reviewedAt,
|
||||||
</if>
|
to_char(r.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
||||||
|
to_char(r.updated_at, 'YYYY-MM-DD HH24:MI') as updatedAt,
|
||||||
|
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sortTime
|
||||||
|
from work_daily_report r
|
||||||
|
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
||||||
|
left join lateral (
|
||||||
|
select string_agg(distinct o.org_name, '、' order by o.org_name) as org_names
|
||||||
|
from sys_tenant_user tu
|
||||||
|
join sys_org o on o.id = tu.org_id
|
||||||
|
where tu.user_id = r.user_id
|
||||||
|
and tu.is_deleted = 0
|
||||||
|
and o.is_deleted = 0
|
||||||
|
) org_info on true
|
||||||
|
left join (
|
||||||
|
select distinct on (report_id)
|
||||||
|
report_id,
|
||||||
|
reviewer_user_id,
|
||||||
|
score,
|
||||||
|
comment_content,
|
||||||
|
reviewed_at
|
||||||
|
from work_daily_report_comment
|
||||||
|
order by report_id, reviewed_at desc nulls last, id desc
|
||||||
|
) rc on rc.report_id = r.id
|
||||||
|
left join sys_user reviewer on reviewer.user_id = rc.reviewer_user_id and reviewer.is_deleted = 0
|
||||||
|
where r.user_id in
|
||||||
|
<foreach collection="visibleUserIds" item="userId" open="(" separator="," close=")">
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
<include refid="dailyReportExportFilters"/>
|
||||||
order by coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) desc nulls last, r.id desc
|
order by coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) desc nulls last, r.id desc
|
||||||
limit #{limit}
|
limit #{limit}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -752,6 +976,7 @@
|
||||||
set latest_progress = #{latestProgress},
|
set latest_progress = #{latestProgress},
|
||||||
next_plan = #{nextPlan},
|
next_plan = #{nextPlan},
|
||||||
stage = coalesce(#{stage}, stage),
|
stage = coalesce(#{stage}, stage),
|
||||||
|
updated_by = #{updatedBy},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where id = #{opportunityId}
|
where id = #{opportunityId}
|
||||||
</update>
|
</update>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
package com.unis.crm.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.unis.crm.common.BusinessException;
|
||||||
|
import com.unis.crm.dto.datascope.UserDataScopeAssignmentDTO;
|
||||||
|
import com.unisbase.security.PermissionService;
|
||||||
|
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||||
|
import com.unisbase.security.SpringSecurityUserProvider;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserDataScopeAdminServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PermissionService permissionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SpringSecurityTenantProvider tenantProvider;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SpringSecurityUserProvider userProvider;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private UserDataScopeAdminService userDataScopeAdminService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAssignment_shouldReplaceCurrentViewerResourceGrants() {
|
||||||
|
when(permissionService.hasPermi("user_data_scope:update")).thenReturn(true);
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(userProvider.getCurrentUserId()).thenReturn(1L);
|
||||||
|
mockUserInTenant(100L, 9L);
|
||||||
|
mockUserInTenant(100L, 2L);
|
||||||
|
mockUserInTenant(100L, 3L);
|
||||||
|
|
||||||
|
UserDataScopeAssignmentDTO payload = new UserDataScopeAssignmentDTO();
|
||||||
|
payload.setTenantId(100L);
|
||||||
|
payload.setViewerUserId(9L);
|
||||||
|
payload.setResourceType("opportunity");
|
||||||
|
payload.setOwnerUserIds(List.of(2L, 3L, 2L, 9L));
|
||||||
|
payload.setRemark("临时授权");
|
||||||
|
|
||||||
|
boolean result = userDataScopeAdminService.saveAssignment(payload);
|
||||||
|
|
||||||
|
assertEquals(true, result);
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("update sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq("OPPORTUNITY"));
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("insert into sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq(2L),
|
||||||
|
eq("OPPORTUNITY"),
|
||||||
|
eq(true),
|
||||||
|
eq(null),
|
||||||
|
eq("临时授权"),
|
||||||
|
eq(1L));
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("insert into sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq(3L),
|
||||||
|
eq("OPPORTUNITY"),
|
||||||
|
eq(true),
|
||||||
|
eq(null),
|
||||||
|
eq("临时授权"),
|
||||||
|
eq(1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAssignment_shouldAllowClearingAllOwners() {
|
||||||
|
when(permissionService.hasPermi("user_data_scope:update")).thenReturn(true);
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
mockUserInTenant(100L, 9L);
|
||||||
|
|
||||||
|
UserDataScopeAssignmentDTO payload = new UserDataScopeAssignmentDTO();
|
||||||
|
payload.setTenantId(100L);
|
||||||
|
payload.setViewerUserId(9L);
|
||||||
|
payload.setResourceType("ALL");
|
||||||
|
payload.setOwnerUserIds(List.of());
|
||||||
|
|
||||||
|
userDataScopeAdminService.saveAssignment(payload);
|
||||||
|
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("update sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq("ALL"));
|
||||||
|
verify(jdbcTemplate, never()).update(
|
||||||
|
contains("insert into sys_user_data_scope_user"),
|
||||||
|
any(Object[].class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAssignment_shouldSaveMultipleResourceTypes() {
|
||||||
|
when(permissionService.hasPermi("user_data_scope:update")).thenReturn(true);
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(userProvider.getCurrentUserId()).thenReturn(1L);
|
||||||
|
mockUserInTenant(100L, 9L);
|
||||||
|
mockUserInTenant(100L, 2L);
|
||||||
|
|
||||||
|
UserDataScopeAssignmentDTO payload = new UserDataScopeAssignmentDTO();
|
||||||
|
payload.setTenantId(100L);
|
||||||
|
payload.setViewerUserId(9L);
|
||||||
|
payload.setResourceTypes(List.of("opportunity", "expansion", "opportunity"));
|
||||||
|
payload.setOwnerUserIds(List.of(2L));
|
||||||
|
|
||||||
|
boolean result = userDataScopeAdminService.saveAssignment(payload);
|
||||||
|
|
||||||
|
assertEquals(true, result);
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("update sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq("OPPORTUNITY"));
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("update sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq("EXPANSION"));
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("insert into sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq(2L),
|
||||||
|
eq("OPPORTUNITY"),
|
||||||
|
eq(true),
|
||||||
|
eq(null),
|
||||||
|
eq(null),
|
||||||
|
eq(1L));
|
||||||
|
verify(jdbcTemplate).update(
|
||||||
|
contains("insert into sys_user_data_scope_user"),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq(2L),
|
||||||
|
eq("EXPANSION"),
|
||||||
|
eq(true),
|
||||||
|
eq(null),
|
||||||
|
eq(null),
|
||||||
|
eq(1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAssignment_shouldRejectCrossTenantRequestWhenCurrentTenantIsConcrete() {
|
||||||
|
when(permissionService.hasPermi("user_data_scope:update")).thenReturn(true);
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
|
||||||
|
UserDataScopeAssignmentDTO payload = new UserDataScopeAssignmentDTO();
|
||||||
|
payload.setTenantId(200L);
|
||||||
|
payload.setViewerUserId(9L);
|
||||||
|
payload.setResourceType("OPPORTUNITY");
|
||||||
|
|
||||||
|
BusinessException exception = assertThrows(
|
||||||
|
BusinessException.class,
|
||||||
|
() -> userDataScopeAdminService.saveAssignment(payload));
|
||||||
|
|
||||||
|
assertEquals("只能配置当前租户的数据授权", exception.getMessage());
|
||||||
|
verify(jdbcTemplate, never()).update(contains("sys_user_data_scope_user"), org.mockito.ArgumentMatchers.<Object[]>any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteGrant_shouldSoftDeleteTenantScopedGrant() {
|
||||||
|
when(permissionService.hasPermi("user_data_scope:update")).thenReturn(true);
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(jdbcTemplate.update(contains("where id = ?"), eq(88L), eq(100L))).thenReturn(1);
|
||||||
|
|
||||||
|
boolean result = userDataScopeAdminService.deleteGrant(100L, 88L);
|
||||||
|
|
||||||
|
assertEquals(true, result);
|
||||||
|
verify(jdbcTemplate).update(contains("update sys_user_data_scope_user"), eq(88L), eq(100L));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockUserInTenant(Long tenantId, Long userId) {
|
||||||
|
when(jdbcTemplate.queryForObject(
|
||||||
|
contains("from sys_user u"),
|
||||||
|
eq(Integer.class),
|
||||||
|
eq(tenantId),
|
||||||
|
eq(userId)))
|
||||||
|
.thenReturn(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.unis.crm.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
|
import com.unisbase.dto.DataScopeRuleDTO;
|
||||||
|
import com.unisbase.service.DataScopeService;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CrmDataVisibilityServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DataScopeService dataScopeService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CrmDataVisibilityServiceImpl service;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveVisibility_shouldMergeBaseScopeAndExplicitUserGrants() {
|
||||||
|
DataScopeRuleDTO rule = new DataScopeRuleDTO();
|
||||||
|
rule.setCreatorUserIds(Arrays.asList(2L, 3L, 2L, null, -1L));
|
||||||
|
when(dataScopeService.resolveUserScope(9L, 100L)).thenReturn(rule);
|
||||||
|
when(jdbcTemplate.queryForList(
|
||||||
|
anyString(),
|
||||||
|
eq(Long.class),
|
||||||
|
eq(100L),
|
||||||
|
eq(9L),
|
||||||
|
eq(CrmDataVisibilityService.RESOURCE_ALL),
|
||||||
|
eq(CrmDataVisibilityService.RESOURCE_OPPORTUNITY)))
|
||||||
|
.thenReturn(List.of(3L, 4L, 5L));
|
||||||
|
|
||||||
|
DataVisibility visibility = service.resolveVisibility(9L, 100L, CrmDataVisibilityService.RESOURCE_OPPORTUNITY);
|
||||||
|
|
||||||
|
assertFalse(visibility.allDataAccess());
|
||||||
|
assertEquals(List.of(2L, 3L, 4L, 5L), visibility.visibleOwnerUserIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveVisibility_shouldKeepAllAccessWhenBaseScopeIsAll() {
|
||||||
|
DataScopeRuleDTO rule = new DataScopeRuleDTO();
|
||||||
|
rule.setAllAccess(true);
|
||||||
|
when(dataScopeService.resolveUserScope(9L, 100L)).thenReturn(rule);
|
||||||
|
|
||||||
|
DataVisibility visibility = service.resolveVisibility(9L, 100L, CrmDataVisibilityService.RESOURCE_OPPORTUNITY);
|
||||||
|
|
||||||
|
assertTrue(visibility.allDataAccess());
|
||||||
|
assertEquals(List.of(), visibility.visibleOwnerUserIds());
|
||||||
|
verify(jdbcTemplate, never()).queryForList(anyString(), eq(Long.class), org.mockito.ArgumentMatchers.<Object>any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ import com.unis.crm.dto.expansion.SalesExpansionItemDTO;
|
||||||
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
|
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
|
||||||
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
|
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
|
||||||
import com.unis.crm.mapper.ExpansionMapper;
|
import com.unis.crm.mapper.ExpansionMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unisbase.security.PermissionService;
|
import com.unisbase.security.PermissionService;
|
||||||
import com.unisbase.security.SpringSecurityTenantProvider;
|
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
@ -41,9 +43,34 @@ class ExpansionServiceImplTest {
|
||||||
@Mock
|
@Mock
|
||||||
private PermissionService permissionService;
|
private PermissionService permissionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CrmDataVisibilityService crmDataVisibilityService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ExpansionServiceImpl expansionService;
|
private ExpansionServiceImpl expansionService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getOverview_shouldIncludeExplicitExpansionGrants() {
|
||||||
|
SalesExpansionItemDTO ownedItem = new SalesExpansionItemDTO();
|
||||||
|
ownedItem.setId(11L);
|
||||||
|
ownedItem.setName("本人拓展");
|
||||||
|
SalesExpansionItemDTO grantedItem = new SalesExpansionItemDTO();
|
||||||
|
grantedItem.setId(12L);
|
||||||
|
grantedItem.setName("授权拓展");
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(crmDataVisibilityService.resolveVisibility(29L, 100L, CrmDataVisibilityService.RESOURCE_EXPANSION))
|
||||||
|
.thenReturn(new DataVisibility(false, List.of(29L, 36L)));
|
||||||
|
when(expansionMapper.selectSalesExpansions(29L, null)).thenReturn(List.of(ownedItem));
|
||||||
|
when(expansionMapper.selectSalesExpansionsByOwnerUserIds(List.of(36L), null)).thenReturn(List.of(grantedItem));
|
||||||
|
when(expansionMapper.selectChannelExpansions(29L, null)).thenReturn(List.of());
|
||||||
|
when(expansionMapper.selectChannelExpansionsByOwnerUserIds(List.of(36L), null)).thenReturn(List.of());
|
||||||
|
ExpansionOverviewDTO result = expansionService.getOverview(29L, null);
|
||||||
|
|
||||||
|
assertEquals(2, result.getSalesItems().size());
|
||||||
|
assertEquals(12L, result.getSalesItems().get(1).getId());
|
||||||
|
verify(expansionMapper).selectSalesExpansionsByOwnerUserIds(List.of(36L), null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createSalesExpansion_shouldRejectDuplicateEmployeeNo() {
|
void createSalesExpansion_shouldRejectDuplicateEmployeeNo() {
|
||||||
CreateSalesExpansionRequest request = buildCreateSalesRequest();
|
CreateSalesExpansionRequest request = buildCreateSalesRequest();
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ import com.unis.crm.dto.opportunity.OpportunityOmsPushDataDTO;
|
||||||
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
|
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
|
||||||
import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest;
|
import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest;
|
||||||
import com.unis.crm.mapper.OpportunityMapper;
|
import com.unis.crm.mapper.OpportunityMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unis.crm.service.OmsClient;
|
import com.unis.crm.service.OmsClient;
|
||||||
import com.unisbase.dto.DataScopeRuleDTO;
|
|
||||||
import com.unisbase.security.PermissionService;
|
import com.unisbase.security.PermissionService;
|
||||||
import com.unisbase.service.DataScopeService;
|
|
||||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
@ -50,7 +50,7 @@ class OpportunityServiceImplTest {
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private DataScopeService dataScopeService;
|
private CrmDataVisibilityService crmDataVisibilityService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private UnisBaseTenantProvider tenantProvider;
|
private UnisBaseTenantProvider tenantProvider;
|
||||||
|
|
@ -217,10 +217,9 @@ class OpportunityServiceImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getOverview_shouldKeepConfiguredDataScopeAndIncludePreSalesVisibility() {
|
void getOverview_shouldKeepConfiguredDataScopeAndIncludePreSalesVisibility() {
|
||||||
DataScopeRuleDTO rule = new DataScopeRuleDTO();
|
|
||||||
rule.setCreatorUserIds(List.of(2L, 3L, 2L));
|
|
||||||
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
when(dataScopeService.resolveUserScope(9L, 100L)).thenReturn(rule);
|
when(crmDataVisibilityService.resolveVisibility(9L, 100L, CrmDataVisibilityService.RESOURCE_OPPORTUNITY))
|
||||||
|
.thenReturn(new DataVisibility(false, List.of(2L, 3L, 2L)));
|
||||||
CurrentUserAccountDTO currentUser = new CurrentUserAccountDTO();
|
CurrentUserAccountDTO currentUser = new CurrentUserAccountDTO();
|
||||||
currentUser.setUserId(9L);
|
currentUser.setUserId(9L);
|
||||||
currentUser.setUsername("presales.zhang");
|
currentUser.setUsername("presales.zhang");
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,21 @@ import com.unis.crm.mapper.OpportunityMapper;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import com.unis.crm.mapper.ProfileMapper;
|
import com.unis.crm.mapper.ProfileMapper;
|
||||||
import com.unis.crm.mapper.WorkMapper;
|
import com.unis.crm.mapper.WorkMapper;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService;
|
||||||
|
import com.unis.crm.service.CrmDataVisibilityService.DataVisibility;
|
||||||
import com.unis.crm.service.ReportReminderService;
|
import com.unis.crm.service.ReportReminderService;
|
||||||
|
import com.unisbase.service.SysPermissionService;
|
||||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Set;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
|
@ -56,6 +62,15 @@ class WorkServiceImplTest {
|
||||||
@Mock
|
@Mock
|
||||||
private UnisBaseTenantProvider tenantProvider;
|
private UnisBaseTenantProvider tenantProvider;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CrmDataVisibilityService crmDataVisibilityService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SysPermissionService sysPermissionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
private WorkServiceImpl workService;
|
private WorkServiceImpl workService;
|
||||||
|
|
||||||
private WorkReportProperties workReportProperties;
|
private WorkReportProperties workReportProperties;
|
||||||
|
|
@ -63,7 +78,19 @@ class WorkServiceImplTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
workReportProperties = new WorkReportProperties();
|
workReportProperties = new WorkReportProperties();
|
||||||
workService = new WorkServiceImpl(workMapper, opportunityMapper, profileMapper, reportReminderService, workReportProperties, new ObjectMapper(), tenantProvider, "build/test-uploads", "");
|
workService = new WorkServiceImpl(
|
||||||
|
workMapper,
|
||||||
|
opportunityMapper,
|
||||||
|
profileMapper,
|
||||||
|
reportReminderService,
|
||||||
|
workReportProperties,
|
||||||
|
new ObjectMapper(),
|
||||||
|
tenantProvider,
|
||||||
|
crmDataVisibilityService,
|
||||||
|
sysPermissionService,
|
||||||
|
jdbcTemplate,
|
||||||
|
"build/test-uploads",
|
||||||
|
"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -86,6 +113,23 @@ class WorkServiceImplTest {
|
||||||
verify(workMapper).selectReportHistory(3);
|
verify(workMapper).selectReportHistory(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getHistory_shouldIncludeExplicitDailyReportGrants() {
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(crmDataVisibilityService.resolveVisibility(17L, 100L, CrmDataVisibilityService.RESOURCE_DAILY_REPORT))
|
||||||
|
.thenReturn(new DataVisibility(false, List.of(17L, 36L)));
|
||||||
|
when(workMapper.selectReportHistory(3)).thenReturn(List.of(
|
||||||
|
historyItem(21L, "日报", "2026-04-02", "09:15", "本人日报")));
|
||||||
|
when(workMapper.selectReportHistoryByUserIds(List.of(36L), 3)).thenReturn(List.of(
|
||||||
|
historyItem(22L, "日报", "2026-04-03", "09:15", "授权日报")));
|
||||||
|
|
||||||
|
WorkHistoryPageDTO result = workService.getHistory(17L, "report", 1, 2);
|
||||||
|
|
||||||
|
assertEquals(2, result.getItems().size());
|
||||||
|
assertEquals(22L, result.getItems().get(0).getId());
|
||||||
|
verify(workMapper).selectReportHistoryByUserIds(List.of(36L), 3);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getHistory_shouldOnlyQueryCheckInHistoryWhenTypeIsCheckin() {
|
void getHistory_shouldOnlyQueryCheckInHistoryWhenTypeIsCheckin() {
|
||||||
when(workMapper.selectCheckInHistory(2)).thenReturn(List.of(
|
when(workMapper.selectCheckInHistory(2)).thenReturn(List.of(
|
||||||
|
|
@ -167,6 +211,31 @@ class WorkServiceImplTest {
|
||||||
verify(workMapper, never()).selectReportMessageUsers(any(), any(), eq(50));
|
verify(workMapper, never()).selectReportMessageUsers(any(), any(), eq(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDailyReportRequirement_shouldUsePermissionForButtonsAndConfigForObjectSelection() {
|
||||||
|
workReportProperties.getDailyReport().setRequiredRoleCodes(List.of("crm_xs"));
|
||||||
|
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("crm_xs"));
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(100L);
|
||||||
|
when(jdbcTemplate.queryForObject(
|
||||||
|
eq("select count(1) from sys_permission where code = ? and coalesce(is_deleted, 0) = 0"),
|
||||||
|
eq(Integer.class),
|
||||||
|
eq("daily_report:notify_users")))
|
||||||
|
.thenReturn(1);
|
||||||
|
when(jdbcTemplate.queryForObject(
|
||||||
|
eq("select count(1) from sys_permission where code = ? and coalesce(is_deleted, 0) = 0"),
|
||||||
|
eq(Integer.class),
|
||||||
|
eq("daily_report:attachments")))
|
||||||
|
.thenReturn(1);
|
||||||
|
when(sysPermissionService.listPermissionCodesByUserId(17L, 100L))
|
||||||
|
.thenReturn(Set.of("daily_report:notify_users"));
|
||||||
|
|
||||||
|
var requirement = workService.getDailyReportRequirement(17L);
|
||||||
|
|
||||||
|
assertEquals(true, requirement.isObjectSelectionRequired());
|
||||||
|
assertEquals(true, requirement.isNotifyUsersVisible());
|
||||||
|
assertEquals(false, requirement.isAttachmentsVisible());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void saveDailyReport_shouldRejectEmptyContentByDefault() {
|
void saveDailyReport_shouldRejectEmptyContentByDefault() {
|
||||||
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
|
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
|
||||||
|
|
@ -248,7 +317,7 @@ class WorkServiceImplTest {
|
||||||
requestCaptor.getValue().getWorkContent().startsWith("1. 2026-04-28 整理客户资料,完成重点项目复盘"));
|
requestCaptor.getValue().getWorkContent().startsWith("1. 2026-04-28 整理客户资料,完成重点项目复盘"));
|
||||||
verify(workMapper, never()).insertLegacyOpportunityFollowUp(anyLong(), anyLong(), any(), any(), any(), any());
|
verify(workMapper, never()).insertLegacyOpportunityFollowUp(anyLong(), anyLong(), any(), any(), any(), any());
|
||||||
verify(workMapper, never()).insertLegacyExpansionFollowUp(any(), anyLong(), anyLong(), any(), any(), any(), any(), any(), any(), any());
|
verify(workMapper, never()).insertLegacyExpansionFollowUp(any(), anyLong(), anyLong(), any(), any(), any(), any(), any(), any(), any());
|
||||||
verify(workMapper, never()).updateOpportunitySnapshot(anyLong(), any(), any(), any());
|
verify(workMapper, never()).updateOpportunitySnapshot(anyLong(), any(), any(), any(), anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -331,6 +400,47 @@ class WorkServiceImplTest {
|
||||||
assertEquals(List.of("/api/work/checkin-photos/a.jpg", "/api/work/checkin-photos/b.jpg"), result.get(0).getPhotoUrls());
|
assertEquals(List.of("/api/work/checkin-photos/a.jpg", "/api/work/checkin-photos/b.jpg"), result.get(0).getPhotoUrls());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportCheckIns_shouldDeduplicateByIdOnly() {
|
||||||
|
WorkCheckInExportDTO first = new WorkCheckInExportDTO();
|
||||||
|
first.setId(101L);
|
||||||
|
first.setCheckinDate("2026-07-01");
|
||||||
|
first.setCheckinTime("09:30");
|
||||||
|
first.setUserName("张三");
|
||||||
|
first.setSortTime(OffsetDateTime.parse("2026-07-01T09:30:00+08:00"));
|
||||||
|
WorkCheckInExportDTO second = new WorkCheckInExportDTO();
|
||||||
|
second.setId(102L);
|
||||||
|
second.setCheckinDate("2026-07-01");
|
||||||
|
second.setCheckinTime("09:30");
|
||||||
|
second.setUserName("张三");
|
||||||
|
second.setSortTime(OffsetDateTime.parse("2026-07-01T08:30:00+08:00"));
|
||||||
|
WorkCheckInExportDTO duplicate = new WorkCheckInExportDTO();
|
||||||
|
duplicate.setId(101L);
|
||||||
|
duplicate.setCheckinDate("2026-07-01");
|
||||||
|
duplicate.setCheckinTime("09:30");
|
||||||
|
duplicate.setUserName("张三");
|
||||||
|
duplicate.setSortTime(OffsetDateTime.parse("2026-07-01T09:30:00+08:00"));
|
||||||
|
WorkCheckInExportDTO granted = new WorkCheckInExportDTO();
|
||||||
|
granted.setId(103L);
|
||||||
|
granted.setCheckinDate("2026-07-01");
|
||||||
|
granted.setCheckinTime("10:00");
|
||||||
|
granted.setUserName("王五");
|
||||||
|
granted.setSortTime(OffsetDateTime.parse("2026-07-01T10:00:00+08:00"));
|
||||||
|
|
||||||
|
when(workMapper.selectCheckInExportRows(null, null, null, null, null, null, 5000))
|
||||||
|
.thenReturn(List.of(first, second));
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(1L);
|
||||||
|
when(crmDataVisibilityService.resolveVisibility(17L, 1L, CrmDataVisibilityService.RESOURCE_CHECKIN))
|
||||||
|
.thenReturn(new DataVisibility(false, List.of(18L)));
|
||||||
|
when(workMapper.selectCheckInExportRowsByUserIds(List.of(18L), null, null, null, null, null, null, 5000))
|
||||||
|
.thenReturn(List.of(duplicate, granted));
|
||||||
|
|
||||||
|
List<WorkCheckInExportDTO> result = workService.exportCheckIns(17L, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
assertEquals(3, result.size());
|
||||||
|
assertEquals(List.of(103L, 101L, 102L), result.stream().map(WorkCheckInExportDTO::getId).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void exportDailyReports_shouldCleanMetadataAndFilterByBizType() {
|
void exportDailyReports_shouldCleanMetadataAndFilterByBizType() {
|
||||||
WorkDailyReportExportDTO opportunityReport = new WorkDailyReportExportDTO();
|
WorkDailyReportExportDTO opportunityReport = new WorkDailyReportExportDTO();
|
||||||
|
|
@ -364,6 +474,47 @@ class WorkServiceImplTest {
|
||||||
assertEquals("跟进客户", result.get(0).getPlanItems().get(0).getContent());
|
assertEquals("跟进客户", result.get(0).getPlanItems().get(0).getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportDailyReports_shouldDeduplicateByIdOnly() {
|
||||||
|
WorkDailyReportExportDTO first = new WorkDailyReportExportDTO();
|
||||||
|
first.setId(201L);
|
||||||
|
first.setReportDate("2026-07-01");
|
||||||
|
first.setSubmitTime("2026-07-01 18:00");
|
||||||
|
first.setUserName("李四");
|
||||||
|
first.setSortTime(OffsetDateTime.parse("2026-07-01T18:00:00+08:00"));
|
||||||
|
WorkDailyReportExportDTO second = new WorkDailyReportExportDTO();
|
||||||
|
second.setId(202L);
|
||||||
|
second.setReportDate("2026-07-01");
|
||||||
|
second.setSubmitTime("2026-07-01 18:00");
|
||||||
|
second.setUserName("李四");
|
||||||
|
second.setSortTime(OffsetDateTime.parse("2026-07-01T17:00:00+08:00"));
|
||||||
|
WorkDailyReportExportDTO duplicate = new WorkDailyReportExportDTO();
|
||||||
|
duplicate.setId(201L);
|
||||||
|
duplicate.setReportDate("2026-07-01");
|
||||||
|
duplicate.setSubmitTime("2026-07-01 18:00");
|
||||||
|
duplicate.setUserName("李四");
|
||||||
|
duplicate.setSortTime(OffsetDateTime.parse("2026-07-01T18:00:00+08:00"));
|
||||||
|
WorkDailyReportExportDTO granted = new WorkDailyReportExportDTO();
|
||||||
|
granted.setId(203L);
|
||||||
|
granted.setReportDate("2026-07-01");
|
||||||
|
granted.setSubmitTime("2026-07-01 18:30");
|
||||||
|
granted.setUserName("王五");
|
||||||
|
granted.setSortTime(OffsetDateTime.parse("2026-07-01T18:30:00+08:00"));
|
||||||
|
|
||||||
|
when(workMapper.selectDailyReportExportRows(null, null, null, null, null, 5000))
|
||||||
|
.thenReturn(List.of(first, second));
|
||||||
|
when(tenantProvider.getCurrentTenantId()).thenReturn(1L);
|
||||||
|
when(crmDataVisibilityService.resolveVisibility(17L, 1L, CrmDataVisibilityService.RESOURCE_DAILY_REPORT))
|
||||||
|
.thenReturn(new DataVisibility(false, List.of(18L)));
|
||||||
|
when(workMapper.selectDailyReportExportRowsByUserIds(List.of(18L), null, null, null, null, null, 5000))
|
||||||
|
.thenReturn(List.of(duplicate, granted));
|
||||||
|
|
||||||
|
List<WorkDailyReportExportDTO> result = workService.exportDailyReports(17L, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
assertEquals(3, result.size());
|
||||||
|
assertEquals(List.of(203L, 201L, 202L), result.stream().map(WorkDailyReportExportDTO::getId).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void saveDailyReport_shouldNotUseTomorrowPlanAsOpportunityNextAction() {
|
void saveDailyReport_shouldNotUseTomorrowPlanAsOpportunityNextAction() {
|
||||||
CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
|
CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
|
||||||
|
|
@ -542,6 +693,7 @@ class WorkServiceImplTest {
|
||||||
when(opportunityMapper.selectDictLabel("sj_xmjd", "方案交流")).thenReturn(null);
|
when(opportunityMapper.selectDictLabel("sj_xmjd", "方案交流")).thenReturn(null);
|
||||||
when(opportunityMapper.selectDictValueByLabel("sj_xmjd", "方案交流")).thenReturn("solution_discussion");
|
when(opportunityMapper.selectDictValueByLabel("sj_xmjd", "方案交流")).thenReturn("solution_discussion");
|
||||||
when(opportunityMapper.selectDictLabel("sj_xmjd", "solution_discussion")).thenReturn("方案交流");
|
when(opportunityMapper.selectDictLabel("sj_xmjd", "solution_discussion")).thenReturn("方案交流");
|
||||||
|
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("XS_Col"));
|
||||||
when(workMapper.insertDailyReport(eq(17L), any(CreateWorkDailyReportRequest.class), any(LocalDate.class), any()))
|
when(workMapper.insertDailyReport(eq(17L), any(CreateWorkDailyReportRequest.class), any(LocalDate.class), any()))
|
||||||
.thenReturn(1);
|
.thenReturn(1);
|
||||||
when(workMapper.selectTodayReportId(eq(17L), any(LocalDate.class)))
|
when(workMapper.selectTodayReportId(eq(17L), any(LocalDate.class)))
|
||||||
|
|
@ -555,7 +707,47 @@ class WorkServiceImplTest {
|
||||||
eq(101L),
|
eq(101L),
|
||||||
eq("推进中"),
|
eq("推进中"),
|
||||||
eq("继续推进"),
|
eq("继续推进"),
|
||||||
eq("solution_discussion"));
|
eq("solution_discussion"),
|
||||||
|
eq(17L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveDailyReport_shouldNotSyncOpportunitySnapshotForNonSalesRole() {
|
||||||
|
CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
|
||||||
|
WorkReportLineItemRequest lineItem = new WorkReportLineItemRequest();
|
||||||
|
lineItem.setWorkDate("2026-04-28");
|
||||||
|
lineItem.setBizType("opportunity");
|
||||||
|
lineItem.setBizId(101L);
|
||||||
|
lineItem.setBizName("项目A");
|
||||||
|
lineItem.setEditorText("""
|
||||||
|
@商机 项目A
|
||||||
|
# 项目最新进展:推进中
|
||||||
|
# 下一步销售计划:继续推进
|
||||||
|
""");
|
||||||
|
request.setLineItems(List.of(lineItem));
|
||||||
|
|
||||||
|
WorkTomorrowPlanItemRequest planItem = new WorkTomorrowPlanItemRequest();
|
||||||
|
planItem.setContent("这是整篇日报的明日计划");
|
||||||
|
request.setPlanItems(List.of(planItem));
|
||||||
|
request.setWorkContent("占位");
|
||||||
|
request.setTomorrowPlan("这是整篇日报的明日计划");
|
||||||
|
request.setSourceType("manual");
|
||||||
|
|
||||||
|
WorkDailyReportDTO currentReport = new WorkDailyReportDTO();
|
||||||
|
currentReport.setDate("2026-04-28");
|
||||||
|
currentReport.setSubmitTime("2026-04-28 10:00");
|
||||||
|
|
||||||
|
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("SQ_Col"));
|
||||||
|
when(workMapper.insertDailyReport(eq(17L), any(CreateWorkDailyReportRequest.class), any(LocalDate.class), any()))
|
||||||
|
.thenReturn(1);
|
||||||
|
when(workMapper.selectTodayReportId(eq(17L), any(LocalDate.class)))
|
||||||
|
.thenReturn(null, 501L);
|
||||||
|
when(workMapper.selectTodayReport(eq(17L), any(LocalDate.class)))
|
||||||
|
.thenReturn(currentReport);
|
||||||
|
|
||||||
|
workService.saveDailyReport(17L, request);
|
||||||
|
|
||||||
|
verify(workMapper, never()).updateOpportunitySnapshot(anyLong(), any(), any(), any(), anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
private WorkHistoryItemDTO historyItem(Long id, String type, String date, String time, String content) {
|
private WorkHistoryItemDTO historyItem(Long id, String type, String date, String time, String content) {
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,7 @@ export interface OpportunityFollowUp {
|
||||||
communicationContent?: string;
|
communicationContent?: string;
|
||||||
nextAction?: string;
|
nextAction?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
roleCodes?: string;
|
||||||
attachments?: WorkReportAttachment[];
|
attachments?: WorkReportAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -816,12 +817,14 @@ interface ApiErrorBody {
|
||||||
|
|
||||||
const LOGIN_PATH = "/login";
|
const LOGIN_PATH = "/login";
|
||||||
const USER_CONTEXT_CACHE_TTL_MS = 2 * 60 * 1000;
|
const USER_CONTEXT_CACHE_TTL_MS = 2 * 60 * 1000;
|
||||||
|
const REFRESH_AHEAD_MS = 60 * 1000;
|
||||||
const CURRENT_USER_CACHE_KEY = "auth-cache:current-user";
|
const CURRENT_USER_CACHE_KEY = "auth-cache:current-user";
|
||||||
const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
|
const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
|
||||||
const MY_PERMISSIONS_CACHE_KEY = "auth-cache:my-permissions";
|
const MY_PERMISSIONS_CACHE_KEY = "auth-cache:my-permissions";
|
||||||
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
||||||
const inFlightRequestCache = new Map<string, Promise<unknown>>();
|
const inFlightRequestCache = new Map<string, Promise<unknown>>();
|
||||||
let authExpiryTimer: number | null = null;
|
let authExpiryTimer: number | null = null;
|
||||||
|
let refreshTokenPromise: Promise<string | null> | null = null;
|
||||||
const AUTH_REQUIRED_MESSAGE_PATTERNS = [
|
const AUTH_REQUIRED_MESSAGE_PATTERNS = [
|
||||||
"登录已失效",
|
"登录已失效",
|
||||||
"重新登录",
|
"重新登录",
|
||||||
|
|
@ -894,6 +897,15 @@ function hasAccessTokenExpired(token?: string | null, now = Date.now()) {
|
||||||
return expiresAt <= now;
|
return expiresAt <= now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAccessTokenExpiringSoon(token?: string | null, now = Date.now()) {
|
||||||
|
const expiresAt = getAccessTokenExpiresAt(token);
|
||||||
|
if (expiresAt === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expiresAt - now <= REFRESH_AHEAD_MS;
|
||||||
|
}
|
||||||
|
|
||||||
function redirectToLoginForTimeout() {
|
function redirectToLoginForTimeout() {
|
||||||
const currentUrl = new URL(window.location.href);
|
const currentUrl = new URL(window.location.href);
|
||||||
if (currentUrl.pathname === LOGIN_PATH && currentUrl.searchParams.get("timeout") === "1") {
|
if (currentUrl.pathname === LOGIN_PATH && currentUrl.searchParams.get("timeout") === "1") {
|
||||||
|
|
@ -927,6 +939,70 @@ function handleUnauthorizedResponse() {
|
||||||
redirectToLoginForTimeout();
|
redirectToLoginForTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistTokenPair(payload: Pick<TokenResponse, "accessToken" | "refreshToken" | "availableTenants">) {
|
||||||
|
localStorage.setItem("accessToken", payload.accessToken);
|
||||||
|
localStorage.setItem("refreshToken", payload.refreshToken);
|
||||||
|
|
||||||
|
if (payload.availableTenants) {
|
||||||
|
localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenPayload = getAccessTokenPayload(payload.accessToken);
|
||||||
|
if (tokenPayload?.tenantId !== undefined) {
|
||||||
|
localStorage.setItem("activeTenantId", String(tokenPayload.tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAuthExpiryTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAccessToken(): Promise<string | null> {
|
||||||
|
if (refreshTokenPromise) {
|
||||||
|
return refreshTokenPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshToken = localStorage.getItem("refreshToken");
|
||||||
|
if (!refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTokenPromise = request<TokenResponse>("/api/sys/auth/refresh", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
if (!payload.accessToken || !payload.refreshToken) {
|
||||||
|
throw new Error("刷新登录态失败");
|
||||||
|
}
|
||||||
|
persistTokenPair(payload);
|
||||||
|
return payload.accessToken;
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
.finally(() => {
|
||||||
|
refreshTokenPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return refreshTokenPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFreshAccessToken(force = false): Promise<string | null> {
|
||||||
|
const token = localStorage.getItem("accessToken");
|
||||||
|
if (!force && token && !isAccessTokenExpiringSoon(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshedToken = await refreshAccessToken();
|
||||||
|
if (refreshedToken) {
|
||||||
|
return refreshedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token || hasAccessTokenExpired(token) || force) {
|
||||||
|
handleUnauthorizedResponse();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
function syncAuthExpiryTimer() {
|
function syncAuthExpiryTimer() {
|
||||||
clearAuthExpiryTimer();
|
clearAuthExpiryTimer();
|
||||||
|
|
||||||
|
|
@ -937,13 +1013,14 @@ function syncAuthExpiryTimer() {
|
||||||
|
|
||||||
const expiresAt = getAccessTokenExpiresAt(token);
|
const expiresAt = getAccessTokenExpiresAt(token);
|
||||||
if (expiresAt === null || expiresAt <= Date.now()) {
|
if (expiresAt === null || expiresAt <= Date.now()) {
|
||||||
handleUnauthorizedResponse();
|
void ensureFreshAccessToken(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshInMs = Math.max(0, expiresAt - Date.now() - REFRESH_AHEAD_MS);
|
||||||
authExpiryTimer = window.setTimeout(() => {
|
authExpiryTimer = window.setTimeout(() => {
|
||||||
handleUnauthorizedResponse();
|
void ensureFreshAccessToken(true);
|
||||||
}, expiresAt - Date.now());
|
}, refreshInMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryParseApiBody<T>(rawText: string, contentType?: string | null): (ApiEnvelope<T> & ApiErrorBody) | null {
|
function tryParseApiBody<T>(rawText: string, contentType?: string | null): (ApiEnvelope<T> & ApiErrorBody) | null {
|
||||||
|
|
@ -1045,6 +1122,10 @@ async function request<T>(input: string, init?: RequestInit, withAuth = false):
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withAuth) {
|
if (withAuth) {
|
||||||
|
const token = await ensureFreshAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("登录已失效,请重新登录");
|
||||||
|
}
|
||||||
applyAuthHeaders(headers);
|
applyAuthHeaders(headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1083,12 +1164,26 @@ async function request<T>(input: string, init?: RequestInit, withAuth = false):
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWithAuth(input: string, init?: RequestInit) {
|
export async function fetchWithAuth(input: string, init?: RequestInit) {
|
||||||
|
const token = await ensureFreshAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("登录已失效,请重新登录");
|
||||||
|
}
|
||||||
const response = await fetch(input, {
|
const response = await fetch(input, {
|
||||||
...init,
|
...init,
|
||||||
headers: applyAuthHeaders(new Headers(init?.headers)),
|
headers: applyAuthHeaders(new Headers(init?.headers)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldTreatAsUnauthorized(response)) {
|
if (shouldTreatAsUnauthorized(response)) {
|
||||||
|
const refreshedToken = await refreshAccessToken();
|
||||||
|
if (refreshedToken) {
|
||||||
|
const retryResponse = await fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers: applyAuthHeaders(new Headers(init?.headers)),
|
||||||
|
});
|
||||||
|
if (!shouldTreatAsUnauthorized(retryResponse)) {
|
||||||
|
return retryResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
handleUnauthorizedResponse();
|
handleUnauthorizedResponse();
|
||||||
throw new Error("登录已失效,请重新登录");
|
throw new Error("登录已失效,请重新登录");
|
||||||
}
|
}
|
||||||
|
|
@ -1098,7 +1193,7 @@ export async function fetchWithAuth(input: string, init?: RequestInit) {
|
||||||
|
|
||||||
export function isAuthed() {
|
export function isAuthed() {
|
||||||
const token = localStorage.getItem("accessToken");
|
const token = localStorage.getItem("accessToken");
|
||||||
return Boolean(token) && !hasAccessTokenExpired(token);
|
return (Boolean(token) && !hasAccessTokenExpired(token)) || Boolean(localStorage.getItem("refreshToken"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAuth() {
|
export function clearAuth() {
|
||||||
|
|
@ -1114,23 +1209,8 @@ export function clearAuth() {
|
||||||
|
|
||||||
export function persistLogin(payload: TokenResponse, username: string) {
|
export function persistLogin(payload: TokenResponse, username: string) {
|
||||||
clearCachedAuthContext();
|
clearCachedAuthContext();
|
||||||
localStorage.setItem("accessToken", payload.accessToken);
|
|
||||||
localStorage.setItem("refreshToken", payload.refreshToken);
|
|
||||||
localStorage.setItem("username", username);
|
localStorage.setItem("username", username);
|
||||||
|
persistTokenPair(payload);
|
||||||
if (payload.availableTenants) {
|
|
||||||
localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants));
|
|
||||||
try {
|
|
||||||
const tokenPayload = getAccessTokenPayload(payload.accessToken);
|
|
||||||
if (tokenPayload?.tenantId !== undefined) {
|
|
||||||
localStorage.setItem("activeTenantId", String(tokenPayload.tenantId));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem("activeTenantId");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncAuthExpiryTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredUserId() {
|
function getStoredUserId() {
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,9 @@ type OpportunityExportFilters = {
|
||||||
operatorName?: string;
|
operatorName?: string;
|
||||||
hasSalesExpansion?: string;
|
hasSalesExpansion?: string;
|
||||||
hasChannelExpansion?: string;
|
hasChannelExpansion?: string;
|
||||||
selectedFields?: OpportunityExportFieldKey[];
|
selectedFields?: LegacyOpportunityExportFieldKey[];
|
||||||
};
|
};
|
||||||
|
type LegacyOpportunityExportFieldKey = OpportunityExportFieldKey | "followUps";
|
||||||
type OpportunityExportFieldKey =
|
type OpportunityExportFieldKey =
|
||||||
| "code"
|
| "code"
|
||||||
| "name"
|
| "name"
|
||||||
|
|
@ -119,7 +120,11 @@ type OpportunityExportFieldKey =
|
||||||
| "isPoc"
|
| "isPoc"
|
||||||
| "latestProgress"
|
| "latestProgress"
|
||||||
| "nextPlan"
|
| "nextPlan"
|
||||||
| "followUps"
|
| "salesFollowUps"
|
||||||
|
| "preSalesFollowUps"
|
||||||
|
| "afterSalesFollowUps"
|
||||||
|
| "productFollowUps"
|
||||||
|
| "businessFollowUps"
|
||||||
| "salesExpansionName"
|
| "salesExpansionName"
|
||||||
| "salesExpansionIntent"
|
| "salesExpansionIntent"
|
||||||
| "salesExpansionActive"
|
| "salesExpansionActive"
|
||||||
|
|
@ -209,7 +214,7 @@ function loadOpportunityExportPreferences(): OpportunityExportFilters {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(rawValue);
|
const parsed = JSON.parse(rawValue);
|
||||||
return typeof parsed === "object" && parsed ? parsed as OpportunityExportFilters : {};
|
return typeof parsed === "object" && parsed ? normalizeOpportunityExportFilters(parsed as OpportunityExportFilters) : {};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -220,12 +225,22 @@ function persistOpportunityExportPreferences(filters: OpportunityExportFilters)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY, JSON.stringify(filters));
|
window.localStorage.setItem(OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY, JSON.stringify(normalizeOpportunityExportFilters(filters)));
|
||||||
} catch {
|
} catch {
|
||||||
// ignore storage failures
|
// ignore storage failures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpportunityExportFilters(filters: OpportunityExportFilters): OpportunityExportFilters {
|
||||||
|
if (!filters.selectedFields) {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...filters,
|
||||||
|
selectedFields: normalizeOpportunityExportFieldKeys(filters.selectedFields),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOpportunityExportText(value?: string | number | boolean | null) {
|
function normalizeOpportunityExportText(value?: string | number | boolean | null) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -290,7 +305,11 @@ const opportunityExportColumns: OpportunityExportColumn[] = [
|
||||||
{ key: "competitorName", label: "竞争对手", value: (item) => normalizeOpportunityExportText(item.competitorName) },
|
{ key: "competitorName", label: "竞争对手", value: (item) => normalizeOpportunityExportText(item.competitorName) },
|
||||||
{ key: "latestProgress", label: "项目最新进展", kind: "longText", value: (item) => normalizeOpportunityExportText(item.latestProgress) },
|
{ key: "latestProgress", label: "项目最新进展", kind: "longText", value: (item) => normalizeOpportunityExportText(item.latestProgress) },
|
||||||
{ key: "nextPlan", label: OPPORTUNITY_NEXT_PLAN_LABEL, kind: "longText", value: (item) => normalizeOpportunityExportText(item.nextPlan) },
|
{ key: "nextPlan", label: OPPORTUNITY_NEXT_PLAN_LABEL, kind: "longText", value: (item) => normalizeOpportunityExportText(item.nextPlan) },
|
||||||
{ key: "followUps", label: "跟进记录", kind: "followup", value: (item, relatedSales, relatedChannel) => buildOpportunityFollowUpExportText(item, relatedSales, relatedChannel) },
|
{ key: "salesFollowUps", label: "销售跟进记录", kind: "followup", value: (item) => buildOpportunityFollowUpExportTextByRole(item, "XS_COL") },
|
||||||
|
{ key: "preSalesFollowUps", label: "售前跟进记录", kind: "followup", value: (item) => buildOpportunityFollowUpExportTextByRole(item, "SQ_COL") },
|
||||||
|
{ key: "afterSalesFollowUps", label: "售后跟进记录", kind: "followup", value: (item) => buildOpportunityFollowUpExportTextByRole(item, "SH_COL") },
|
||||||
|
{ key: "productFollowUps", label: "产品跟进记录", kind: "followup", value: (item) => buildOpportunityFollowUpExportTextByRole(item, "CP_COL") },
|
||||||
|
{ key: "businessFollowUps", label: "商务跟进记录", kind: "followup", value: (item) => buildOpportunityFollowUpExportTextByRole(item, "SW_COL") },
|
||||||
{ key: "salesExpansionName", label: "销售拓展人员姓名", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name) },
|
{ key: "salesExpansionName", label: "销售拓展人员姓名", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name) },
|
||||||
{ key: "salesExpansionIntent", label: "销售拓展人员合作意向", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent) },
|
{ key: "salesExpansionIntent", label: "销售拓展人员合作意向", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent) },
|
||||||
{ key: "salesExpansionActive", label: "销售拓展人员是否在职", value: (item, relatedSales) => {
|
{ key: "salesExpansionActive", label: "销售拓展人员是否在职", value: (item, relatedSales) => {
|
||||||
|
|
@ -327,20 +346,23 @@ const defaultOpportunityExportFields: OpportunityExportFieldKey[] = [
|
||||||
"competitorName",
|
"competitorName",
|
||||||
"latestProgress",
|
"latestProgress",
|
||||||
"nextPlan",
|
"nextPlan",
|
||||||
"followUps",
|
"salesFollowUps",
|
||||||
"isPoc",
|
"isPoc",
|
||||||
];
|
];
|
||||||
|
|
||||||
function resolveSelectedOpportunityFields(selectedFields: OpportunityExportFieldKey[] | undefined) {
|
function resolveSelectedOpportunityFields(selectedFields: LegacyOpportunityExportFieldKey[] | undefined) {
|
||||||
return selectedFields === undefined ? defaultOpportunityExportFields : selectedFields;
|
return selectedFields === undefined ? defaultOpportunityExportFields : normalizeOpportunityExportFieldKeys(selectedFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOpportunityFollowUpExportText(
|
function normalizeOpportunityExportFieldKeys(selectedFields: LegacyOpportunityExportFieldKey[]) {
|
||||||
item: OpportunityItem,
|
const validFieldKeys = new Set(opportunityExportColumns.map((column) => column.key));
|
||||||
_relatedSales: SalesExpansionItem | null,
|
const migratedFields = selectedFields.map((field) => field === "followUps" ? "salesFollowUps" : field);
|
||||||
_relatedChannel: ChannelExpansionItem | null,
|
return Array.from(new Set(migratedFields)).filter((field): field is OpportunityExportFieldKey => validFieldKeys.has(field as OpportunityExportFieldKey));
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
function buildOpportunityFollowUpExportTextByRole(item: OpportunityItem, roleCode: string) {
|
||||||
return (item.followUps ?? [])
|
return (item.followUps ?? [])
|
||||||
|
.filter((record) => opportunityFollowUpHasRole(record, roleCode))
|
||||||
.map((record) => {
|
.map((record) => {
|
||||||
const summary = getOpportunityFollowUpSummary(record);
|
const summary = getOpportunityFollowUpSummary(record);
|
||||||
const lines = [
|
const lines = [
|
||||||
|
|
@ -353,6 +375,14 @@ function buildOpportunityFollowUpExportText(
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function opportunityFollowUpHasRole(record: OpportunityFollowUp, roleCode: string) {
|
||||||
|
const normalizedTarget = roleCode.trim().toUpperCase();
|
||||||
|
return (record.roleCodes ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((code) => code.trim().toUpperCase())
|
||||||
|
.includes(normalizedTarget);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOpportunityExportFilterText(value?: string | number | boolean | null) {
|
function normalizeOpportunityExportFilterText(value?: string | number | boolean | null) {
|
||||||
return normalizeOpportunityExportText(value).toLowerCase();
|
return normalizeOpportunityExportText(value).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
@ -2481,10 +2511,14 @@ export default function Opportunities() {
|
||||||
wrapText: rowNumber > 1 && columns[columnNumber - 1]?.kind === "followup",
|
wrapText: rowNumber > 1 && columns[columnNumber - 1]?.kind === "followup",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const followUpColumnIndex = columns.findIndex((column) => column.kind === "followup") + 1;
|
const followUpColumnIndexes = columns
|
||||||
if (rowNumber > 1 && followUpColumnIndex > 0) {
|
.map((column, index) => column.kind === "followup" ? index + 1 : 0)
|
||||||
const followUpText = normalizeOpportunityExportText(row.getCell(followUpColumnIndex).value as string | null | undefined);
|
.filter((columnIndex) => columnIndex > 0);
|
||||||
const lineCount = followUpText ? followUpText.split("\n").length : 1;
|
if (rowNumber > 1 && followUpColumnIndexes.length > 0) {
|
||||||
|
const lineCount = followUpColumnIndexes.reduce((maxLineCount, columnIndex) => {
|
||||||
|
const followUpText = normalizeOpportunityExportText(row.getCell(columnIndex).value as string | null | undefined);
|
||||||
|
return Math.max(maxLineCount, followUpText ? followUpText.split("\n").length : 1);
|
||||||
|
}, 1);
|
||||||
row.height = Math.max(22, lineCount * 16);
|
row.height = Math.max(22, lineCount * 16);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>UnisBase - 智能会议系统</title>
|
<title>UnisBase - 智能会议系统</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DNoKV8lp.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bk1lir76.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import http from "./http";
|
import http from "./http";
|
||||||
import {
|
import {
|
||||||
BotCredential, DeviceInfo, RoleDataScope, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
BotCredential, DeviceInfo, RoleDataScope, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
||||||
PermissionNode
|
PermissionNode, UserDataScopeAssignment, UserDataScopeGrant, UserDataScopeUser
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
export async function pageParams(params: SysParamQuery) {
|
export async function pageParams(params: SysParamQuery) {
|
||||||
|
|
@ -199,6 +199,31 @@ export async function unbindUserFromRole(roleId: number, userId: number) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listUserDataScopeUsers(params?: { tenantId?: number }) {
|
||||||
|
const resp = await http.get("/sys/api/admin/user-data-scope/users", { params });
|
||||||
|
return resp.data.data as UserDataScopeUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listUserDataScopeGrants(params?: { tenantId?: number; viewerUserId?: number; resourceType?: string }) {
|
||||||
|
const resp = await http.get("/sys/api/admin/user-data-scope/grants", { params });
|
||||||
|
return resp.data.data as UserDataScopeGrant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserDataScopeAssignment(params: { tenantId?: number; viewerUserId: number; resourceType: string }) {
|
||||||
|
const resp = await http.get("/sys/api/admin/user-data-scope/assignment", { params });
|
||||||
|
return resp.data.data as UserDataScopeAssignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveUserDataScopeAssignment(payload: UserDataScopeAssignment) {
|
||||||
|
const resp = await http.put("/sys/api/admin/user-data-scope/assignment", payload);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUserDataScopeGrant(id: number, params?: { tenantId?: number }) {
|
||||||
|
const resp = await http.delete(`/sys/api/admin/user-data-scope/grants/${id}`, { params });
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchLogs(params: any) {
|
export async function fetchLogs(params: any) {
|
||||||
const resp = await http.get("/sys/api/logs", { params });
|
const resp = await http.get("/sys/api/logs", { params });
|
||||||
return resp.data.data;
|
return resp.data.data;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export { default as SysParams } from "./system/sys-params";
|
||||||
export { default as PlatformSettings } from "./system/platform-settings";
|
export { default as PlatformSettings } from "./system/platform-settings";
|
||||||
export { default as ReportReminderSettings } from "./system/report-reminder-settings";
|
export { default as ReportReminderSettings } from "./system/report-reminder-settings";
|
||||||
export { default as Logs } from "./system/logs";
|
export { default as Logs } from "./system/logs";
|
||||||
|
export { default as UserDataScope } from "./system/user-data-scope";
|
||||||
export { default as Devices } from "./devices";
|
export { default as Devices } from "./devices";
|
||||||
export { default as UserRoleBinding } from "./bindings/user-role";
|
export { default as UserRoleBinding } from "./bindings/user-role";
|
||||||
export { default as RolePermissionBinding } from "./bindings/role-permission";
|
export { default as RolePermissionBinding } from "./bindings/role-permission";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
.user-data-scope-page {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-overview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-stat {
|
||||||
|
min-height: 72px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-stat__label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-stat__value {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-filter,
|
||||||
|
.user-data-scope-table-card {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-data-scope-table-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.user-data-scope-overview {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,540 @@
|
||||||
|
import { App, Button, Card, Col, DatePicker, Drawer, Form, Input, Popconfirm, Row, Select, Space, Table, Tag, Tooltip, Typography } from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from "@ant-design/icons";
|
||||||
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import { deleteUserDataScopeGrant, listTenants, listUserDataScopeGrants, listUserDataScopeUsers, saveUserDataScopeAssignment } from "@/api";
|
||||||
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
|
import type { SysTenant, UserDataScopeGrant, UserDataScopeUser } from "@/types";
|
||||||
|
import "./index.less";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const resourceOptions = [
|
||||||
|
{ label: "商机数据", value: "OPPORTUNITY" },
|
||||||
|
{ label: "拓展数据", value: "EXPANSION" },
|
||||||
|
{ label: "日报数据", value: "DAILY_REPORT" },
|
||||||
|
{ label: "打卡数据", value: "CHECKIN" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const resourceLabels: Record<string, string> = {
|
||||||
|
ALL: "全部数据",
|
||||||
|
OPPORTUNITY: "商机数据",
|
||||||
|
EXPANSION: "拓展数据",
|
||||||
|
DAILY_REPORT: "日报数据",
|
||||||
|
CHECKIN: "打卡数据",
|
||||||
|
CUSTOMER: "客户数据",
|
||||||
|
WORK: "工作数据"
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserDataScopeGrantGroup {
|
||||||
|
id: string;
|
||||||
|
grantIds: number[];
|
||||||
|
tenantId: number;
|
||||||
|
viewerUserId: number;
|
||||||
|
viewerName: string;
|
||||||
|
ownerUsers: Array<{ userId: number; name: string }>;
|
||||||
|
resourceTypes: string[];
|
||||||
|
enabled: boolean;
|
||||||
|
expireAt?: string;
|
||||||
|
remark?: string;
|
||||||
|
grants: UserDataScopeGrant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveTenantId() {
|
||||||
|
const raw = localStorage.getItem("activeTenantId");
|
||||||
|
const value = raw ? Number(raw) : 0;
|
||||||
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlatformMode() {
|
||||||
|
try {
|
||||||
|
const profileStr = sessionStorage.getItem("userProfile");
|
||||||
|
const profile = profileStr ? JSON.parse(profileStr) : {};
|
||||||
|
return Boolean(profile.isPlatformAdmin && Number(localStorage.getItem("activeTenantId") || profile.tenantId || 0) === 0);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userLabel(user: UserDataScopeUser) {
|
||||||
|
const name = user.displayName || user.username || String(user.userId);
|
||||||
|
return `${name}${user.username && user.username !== name ? ` (${user.username})` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceLabel(value?: string) {
|
||||||
|
return value ? resourceLabels[value] || value : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGroupExpired(group: UserDataScopeGrantGroup) {
|
||||||
|
return Boolean(group.expireAt && new Date(group.expireAt).getTime() <= Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatExpireAt(value?: string) {
|
||||||
|
return value ? value.replace("T", " ").slice(0, 16) : "永久";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGrantGroups(source: UserDataScopeGrant[]) {
|
||||||
|
const buckets = new Map<string, Map<string, UserDataScopeGrant[]>>();
|
||||||
|
for (const grant of source) {
|
||||||
|
const bucketKey = [
|
||||||
|
grant.tenantId,
|
||||||
|
grant.viewerUserId,
|
||||||
|
grant.enabled ? "1" : "0",
|
||||||
|
grant.expireAt || "",
|
||||||
|
grant.remark || ""
|
||||||
|
].join("|");
|
||||||
|
const resourceMap = buckets.get(bucketKey) || new Map<string, UserDataScopeGrant[]>();
|
||||||
|
const resourceGrants = resourceMap.get(grant.resourceType) || [];
|
||||||
|
resourceGrants.push(grant);
|
||||||
|
resourceMap.set(grant.resourceType, resourceGrants);
|
||||||
|
buckets.set(bucketKey, resourceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: UserDataScopeGrantGroup[] = [];
|
||||||
|
for (const [bucketKey, resourceMap] of buckets) {
|
||||||
|
const ownerSetMap = new Map<string, UserDataScopeGrant[]>();
|
||||||
|
for (const resourceGrants of resourceMap.values()) {
|
||||||
|
const ownerKey = resourceGrants
|
||||||
|
.map((grant) => grant.ownerUserId)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.join(",");
|
||||||
|
ownerSetMap.set(ownerKey, [...(ownerSetMap.get(ownerKey) || []), ...resourceGrants]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [ownerKey, groupGrants] of ownerSetMap) {
|
||||||
|
const first = groupGrants[0];
|
||||||
|
const ownerUsers = Array.from(
|
||||||
|
new Map(groupGrants.map((grant) => [grant.ownerUserId, { userId: grant.ownerUserId, name: grant.ownerName }])).values()
|
||||||
|
).sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
|
||||||
|
const resourceTypes = Array.from(new Set(groupGrants.map((grant) => grant.resourceType)))
|
||||||
|
.sort((a, b) => resourceLabel(a).localeCompare(resourceLabel(b), "zh-CN"));
|
||||||
|
|
||||||
|
groups.push({
|
||||||
|
id: `${bucketKey}|${ownerKey}`,
|
||||||
|
grantIds: groupGrants.map((grant) => grant.id),
|
||||||
|
tenantId: first.tenantId,
|
||||||
|
viewerUserId: first.viewerUserId,
|
||||||
|
viewerName: first.viewerName,
|
||||||
|
ownerUsers,
|
||||||
|
resourceTypes,
|
||||||
|
enabled: first.enabled,
|
||||||
|
expireAt: first.expireAt,
|
||||||
|
remark: first.remark,
|
||||||
|
grants: groupGrants
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.sort((a, b) => (
|
||||||
|
a.viewerName.localeCompare(b.viewerName, "zh-CN")
|
||||||
|
|| a.resourceTypes.join(",").localeCompare(b.resourceTypes.join(","), "zh-CN")
|
||||||
|
|| a.ownerUsers.map((owner) => owner.name).join(",").localeCompare(b.ownerUsers.map((owner) => owner.name).join(","), "zh-CN")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserDataScopePage() {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const { can } = usePermission();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserDataScopeUser[]>([]);
|
||||||
|
const [grants, setGrants] = useState<UserDataScopeGrant[]>([]);
|
||||||
|
const [tenants, setTenants] = useState<SysTenant[]>([]);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [editingGrantGroup, setEditingGrantGroup] = useState<UserDataScopeGrantGroup | null>(null);
|
||||||
|
const [selectedTenantId, setSelectedTenantId] = useState<number | undefined>(() => getActiveTenantId());
|
||||||
|
const [query, setQuery] = useState<{ keyword?: string; viewerUserId?: number; resourceType?: string; current: number; pageSize: number }>({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10
|
||||||
|
});
|
||||||
|
const platformMode = useMemo(() => isPlatformMode(), []);
|
||||||
|
|
||||||
|
const loadTenants = useCallback(async () => {
|
||||||
|
if (!platformMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await listTenants({ current: 1, size: 1000 });
|
||||||
|
setTenants(response?.records || []);
|
||||||
|
}, [platformMode]);
|
||||||
|
|
||||||
|
const loadUsers = useCallback(async () => {
|
||||||
|
if (!selectedTenantId) {
|
||||||
|
setUsers([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = await listUserDataScopeUsers({ tenantId: selectedTenantId });
|
||||||
|
setUsers(list || []);
|
||||||
|
}, [selectedTenantId]);
|
||||||
|
|
||||||
|
const loadGrants = useCallback(async () => {
|
||||||
|
if (!selectedTenantId) {
|
||||||
|
setGrants([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = await listUserDataScopeGrants({ tenantId: selectedTenantId });
|
||||||
|
setGrants(list || []);
|
||||||
|
}, [selectedTenantId]);
|
||||||
|
|
||||||
|
const loadPageData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await Promise.all([loadTenants(), loadUsers(), loadGrants()]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loadTenants, loadUsers, loadGrants]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPageData();
|
||||||
|
}, [loadPageData]);
|
||||||
|
|
||||||
|
const userOptions = useMemo(() => users.map((user) => ({
|
||||||
|
label: (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<span>{userLabel(user)}</span>
|
||||||
|
{user.orgName && <Text type="secondary" style={{ fontSize: 12 }}>{user.orgName}</Text>}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
displayLabel: userLabel(user),
|
||||||
|
value: user.userId,
|
||||||
|
searchText: `${user.displayName || ""} ${user.username || ""} ${user.orgName || ""}`
|
||||||
|
})), [users]);
|
||||||
|
|
||||||
|
const grantGroups = useMemo(() => buildGrantGroups(grants), [grants]);
|
||||||
|
|
||||||
|
const filteredGrantGroups = useMemo(() => {
|
||||||
|
const normalizedKeyword = (query.keyword || "").trim().toLowerCase();
|
||||||
|
return grantGroups.filter((group) => {
|
||||||
|
if (query.viewerUserId && group.viewerUserId !== query.viewerUserId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (query.resourceType && !group.resourceTypes.includes(query.resourceType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
group.viewerName,
|
||||||
|
group.remark,
|
||||||
|
String(group.viewerUserId),
|
||||||
|
...group.ownerUsers.flatMap((owner) => [owner.name, String(owner.userId)]),
|
||||||
|
...group.resourceTypes.flatMap((type) => [type, resourceLabel(type)])
|
||||||
|
].filter(Boolean).join(" ").toLowerCase().includes(normalizedKeyword);
|
||||||
|
});
|
||||||
|
}, [grantGroups, query.keyword, query.resourceType, query.viewerUserId]);
|
||||||
|
|
||||||
|
const pagedGrantGroups = useMemo(() => {
|
||||||
|
const start = (query.current - 1) * query.pageSize;
|
||||||
|
return filteredGrantGroups.slice(start, start + query.pageSize);
|
||||||
|
}, [filteredGrantGroups, query.current, query.pageSize]);
|
||||||
|
|
||||||
|
const activeGrantCount = useMemo(
|
||||||
|
() => grantGroups.filter((group) => group.enabled && !isGroupExpired(group)).length,
|
||||||
|
[grantGroups]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDrawerViewerId = Form.useWatch("viewerUserId", form);
|
||||||
|
const ownerOptions = useMemo(
|
||||||
|
() => userOptions.filter((option) => option.value !== selectedDrawerViewerId),
|
||||||
|
[selectedDrawerViewerId, userOptions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditingGrantGroup(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({
|
||||||
|
resourceTypes: ["OPPORTUNITY"],
|
||||||
|
enabled: true,
|
||||||
|
ownerUserIds: []
|
||||||
|
});
|
||||||
|
setDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (record: UserDataScopeGrantGroup) => {
|
||||||
|
setEditingGrantGroup(record);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({
|
||||||
|
viewerUserId: record.viewerUserId,
|
||||||
|
resourceTypes: record.resourceTypes,
|
||||||
|
ownerUserIds: record.ownerUsers.map((owner) => owner.userId),
|
||||||
|
enabled: record.enabled,
|
||||||
|
expireAt: record.expireAt ? dayjs(record.expireAt) : undefined,
|
||||||
|
remark: record.remark
|
||||||
|
});
|
||||||
|
setDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
setEditingGrantGroup(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!selectedTenantId) {
|
||||||
|
message.warning("请先选择租户");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const values = await form.validateFields();
|
||||||
|
const resourceTypes = values.resourceTypes || [];
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
if (editingGrantGroup) {
|
||||||
|
const selectedResourceTypes = new Set(resourceTypes);
|
||||||
|
const removedGrantIds = editingGrantGroup.grants
|
||||||
|
.filter((grant) => !selectedResourceTypes.has(grant.resourceType))
|
||||||
|
.map((grant) => grant.id);
|
||||||
|
await Promise.all(removedGrantIds.map((id) => deleteUserDataScopeGrant(id, { tenantId: selectedTenantId })));
|
||||||
|
}
|
||||||
|
await saveUserDataScopeAssignment({
|
||||||
|
tenantId: selectedTenantId,
|
||||||
|
viewerUserId: values.viewerUserId,
|
||||||
|
resourceType: resourceTypes[0],
|
||||||
|
resourceTypes,
|
||||||
|
ownerUserIds: values.ownerUserIds || [],
|
||||||
|
enabled: values.enabled !== false,
|
||||||
|
expireAt: values.expireAt ? values.expireAt.toISOString() : undefined,
|
||||||
|
remark: values.remark
|
||||||
|
});
|
||||||
|
message.success("数据授权已保存");
|
||||||
|
closeDrawer();
|
||||||
|
await loadGrants();
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (record: UserDataScopeGrantGroup) => {
|
||||||
|
await Promise.all(record.grantIds.map((id) => deleteUserDataScopeGrant(id, { tenantId: selectedTenantId })));
|
||||||
|
message.success("数据授权已删除");
|
||||||
|
await loadGrants();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<UserDataScopeGrantGroup> = [
|
||||||
|
{
|
||||||
|
title: "查看人",
|
||||||
|
dataIndex: "viewerName",
|
||||||
|
width: 180,
|
||||||
|
render: (value: string, record) => (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Text strong>{value}</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>ID: {record.viewerUserId}</Text>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "可见人员",
|
||||||
|
dataIndex: "ownerUsers",
|
||||||
|
width: 260,
|
||||||
|
render: (_: unknown, record) => (
|
||||||
|
<Space wrap size={[6, 6]}>
|
||||||
|
{record.ownerUsers.map((owner) => (
|
||||||
|
<Tag key={owner.userId}>
|
||||||
|
{owner.name}
|
||||||
|
<Text type="secondary" style={{ marginLeft: 4, fontSize: 12 }}>ID: {owner.userId}</Text>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "资源范围",
|
||||||
|
dataIndex: "resourceTypes",
|
||||||
|
width: 220,
|
||||||
|
render: (_: unknown, record) => (
|
||||||
|
<Space wrap size={[6, 6]}>
|
||||||
|
{record.resourceTypes.map((value) => (
|
||||||
|
<Tag key={value} color={value === "ALL" ? "blue" : "geekblue"}>{resourceLabel(value)}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "enabled",
|
||||||
|
width: 100,
|
||||||
|
render: (enabled: boolean, record) => {
|
||||||
|
if (isGroupExpired(record)) {
|
||||||
|
return <Tag color="orange">已过期</Tag>;
|
||||||
|
}
|
||||||
|
return <Tag color={enabled ? "green" : "red"}>{enabled ? "启用" : "停用"}</Tag>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "有效期",
|
||||||
|
dataIndex: "expireAt",
|
||||||
|
width: 170,
|
||||||
|
render: (value?: string) => formatExpireAt(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "备注",
|
||||||
|
dataIndex: "remark",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value?: string) => value ? <Tooltip title={value}>{value}</Tooltip> : "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
width: 120,
|
||||||
|
fixed: "right",
|
||||||
|
render: (_: unknown, record) => (
|
||||||
|
<Space size={4}>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
disabled={!can("user_data_scope:update")}
|
||||||
|
onClick={() => openEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm title="确认删除该组授权?" okText="删除" cancelText="取消" onConfirm={() => handleDelete(record)}>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} disabled={!can("user_data_scope:update")} />
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-page user-data-scope-page">
|
||||||
|
<PageHeader title="数据授权管理" subtitle="查看、添加或删除某个用户可额外查看的人员数据权限。" />
|
||||||
|
|
||||||
|
<div className="user-data-scope-overview">
|
||||||
|
<div className="user-data-scope-stat">
|
||||||
|
<span className="user-data-scope-stat__label">授权记录</span>
|
||||||
|
<span className="user-data-scope-stat__value">{grantGroups.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="user-data-scope-stat">
|
||||||
|
<span className="user-data-scope-stat__label">生效授权</span>
|
||||||
|
<span className="user-data-scope-stat__value">{activeGrantCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="user-data-scope-stat">
|
||||||
|
<span className="user-data-scope-stat__label">授权查看人</span>
|
||||||
|
<span className="user-data-scope-stat__value">{new Set(grants.map((grant) => grant.viewerUserId)).size}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="app-page__filter-card user-data-scope-filter" styles={{ body: { padding: 16 } }}>
|
||||||
|
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||||
|
<Space wrap>
|
||||||
|
{platformMode && (
|
||||||
|
<Select
|
||||||
|
value={selectedTenantId}
|
||||||
|
placeholder="选择租户"
|
||||||
|
options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedTenantId(value);
|
||||||
|
setQuery((prev) => ({ ...prev, current: 1, viewerUserId: undefined }));
|
||||||
|
}}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
placeholder="搜索查看人、可见人员、备注"
|
||||||
|
value={query.keyword}
|
||||||
|
onChange={(event) => setQuery((prev) => ({ ...prev, keyword: event.target.value, current: 1 }))}
|
||||||
|
style={{ width: 260 }}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
placeholder="筛选查看人"
|
||||||
|
value={query.viewerUserId}
|
||||||
|
options={userOptions}
|
||||||
|
optionFilterProp="searchText"
|
||||||
|
optionLabelProp="displayLabel"
|
||||||
|
onChange={(value) => setQuery((prev) => ({ ...prev, viewerUserId: value, current: 1 }))}
|
||||||
|
style={{ width: 220 }}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="资源范围"
|
||||||
|
value={query.resourceType}
|
||||||
|
options={resourceOptions}
|
||||||
|
onChange={(value) => setQuery((prev) => ({ ...prev, resourceType: value, current: 1 }))}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} disabled={!can("user_data_scope:update")} onClick={openCreate}>
|
||||||
|
新建授权
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="app-page__content-card user-data-scope-table-card" styles={{ body: { padding: 0 } }}>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={pagedGrantGroups}
|
||||||
|
size="middle"
|
||||||
|
scroll={{ y: "calc(100vh - 390px)", x: 980 }}
|
||||||
|
pagination={getStandardPagination(
|
||||||
|
filteredGrantGroups.length,
|
||||||
|
query.current,
|
||||||
|
query.pageSize,
|
||||||
|
(page, pageSize) => setQuery((prev) => ({ ...prev, current: page, pageSize }))
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={<span><TeamOutlined className="mr-2" />{editingGrantGroup ? "编辑数据授权" : "新建数据授权"}</span>}
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
width={560}
|
||||||
|
destroyOnHidden
|
||||||
|
footer={<div className="app-page__drawer-footer"><Button onClick={closeDrawer}>取消</Button><Button type="primary" loading={saving} onClick={handleSave}>保存</Button></div>}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" initialValues={{ resourceTypes: ["OPPORTUNITY"], enabled: true, ownerUserIds: [] }}>
|
||||||
|
<Form.Item label="查看人" name="viewerUserId" rules={[{ required: true, message: "请选择查看人" }]}>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
placeholder="选择谁可以看数据"
|
||||||
|
options={userOptions}
|
||||||
|
optionFilterProp="searchText"
|
||||||
|
optionLabelProp="displayLabel"
|
||||||
|
disabled={Boolean(editingGrantGroup)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="资源范围" name="resourceTypes" rules={[{ required: true, message: "请选择资源范围" }]}>
|
||||||
|
<Select mode="multiple" allowClear options={resourceOptions} maxTagCount="responsive" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="可见人员" name="ownerUserIds" rules={[{ required: true, message: "请选择可见人员" }]}>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
placeholder="选择允许查看的数据归属人"
|
||||||
|
options={ownerOptions}
|
||||||
|
optionFilterProp="searchText"
|
||||||
|
optionLabelProp="displayLabel"
|
||||||
|
maxTagCount="responsive"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label="启用状态" name="enabled">
|
||||||
|
<Select options={[{ label: "启用", value: true }, { label: "停用", value: false }]} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label="有效期" name="expireAt">
|
||||||
|
<DatePicker showTime allowClear placeholder="永久" style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item label="备注" name="remark">
|
||||||
|
<Input.TextArea rows={3} maxLength={500} placeholder="例如:临时支持某销售团队的数据查看" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ const SysParams = lazy(() => import("@/pages/system/sys-params"));
|
||||||
const PlatformSettings = lazy(() => import("@/pages/system/platform-settings"));
|
const PlatformSettings = lazy(() => import("@/pages/system/platform-settings"));
|
||||||
const Dictionaries = lazy(() => import("@/pages/system/dictionaries"));
|
const Dictionaries = lazy(() => import("@/pages/system/dictionaries"));
|
||||||
const Logs = lazy(() => import("@/pages/system/logs"));
|
const Logs = lazy(() => import("@/pages/system/logs"));
|
||||||
|
const UserDataScope = lazy(() => import("@/pages/system/user-data-scope"));
|
||||||
const ReportReminderSettings = lazy(() => import("@/features/report-reminder/pages/report-reminder-settings"));
|
const ReportReminderSettings = lazy(() => import("@/features/report-reminder/pages/report-reminder-settings"));
|
||||||
const DashboardAnalyticsSettings = lazy(() => import("@/features/dashboard-analytics/pages/dashboard-analytics-settings"));
|
const DashboardAnalyticsSettings = lazy(() => import("@/features/dashboard-analytics/pages/dashboard-analytics-settings"));
|
||||||
const OwnerTransfer = lazy(() => import("@/features/owner-transfer/pages/owner-transfer"));
|
const OwnerTransfer = lazy(() => import("@/features/owner-transfer/pages/owner-transfer"));
|
||||||
|
|
@ -44,6 +45,7 @@ export const menuRoutes: MenuRoute[] = [
|
||||||
{ path: "/platform-settings", label: "平台设置", element: <LazyPage><PlatformSettings /></LazyPage>, perm: "menu:platform" },
|
{ path: "/platform-settings", label: "平台设置", element: <LazyPage><PlatformSettings /></LazyPage>, perm: "menu:platform" },
|
||||||
{ path: "/dictionaries", label: "字典管理", element: <LazyPage><Dictionaries /></LazyPage>, perm: "menu:dict" },
|
{ path: "/dictionaries", label: "字典管理", element: <LazyPage><Dictionaries /></LazyPage>, perm: "menu:dict" },
|
||||||
{ path: "/logs", label: "日志管理", element: <LazyPage><Logs /></LazyPage>, perm: "menu:logs" },
|
{ path: "/logs", label: "日志管理", element: <LazyPage><Logs /></LazyPage>, perm: "menu:logs" },
|
||||||
|
{ path: "/data-scope-users", label: "数据授权管理", element: <LazyPage><UserDataScope /></LazyPage>, perm: "menu:data-scope-users" },
|
||||||
{ path: "/dashboard-analytics-settings", label: "首页经营分析配置", element: <LazyPage><DashboardAnalyticsSettings /></LazyPage>, perm: "menu:dashboard-analytics-settings" },
|
{ path: "/dashboard-analytics-settings", label: "首页经营分析配置", element: <LazyPage><DashboardAnalyticsSettings /></LazyPage>, perm: "menu:dashboard-analytics-settings" },
|
||||||
{ path: "/report-reminder-settings", label: "日报提醒设置", element: <LazyPage><ReportReminderSettings /></LazyPage>, perm: "menu:report-reminder-settings" },
|
{ path: "/report-reminder-settings", label: "日报提醒设置", element: <LazyPage><ReportReminderSettings /></LazyPage>, perm: "menu:report-reminder-settings" },
|
||||||
{ path: "/owner-transfer", label: "归属人转移", element: <LazyPage><OwnerTransfer /></LazyPage>, perm: "menu:owner-transfer" },
|
{ path: "/owner-transfer", label: "归属人转移", element: <LazyPage><OwnerTransfer /></LazyPage>, perm: "menu:owner-transfer" },
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,41 @@ export interface SysOrg extends BaseEntity {
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserDataScopeUser {
|
||||||
|
userId: number;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
orgName?: string;
|
||||||
|
status?: number;
|
||||||
|
tenantId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDataScopeGrant {
|
||||||
|
id: number;
|
||||||
|
tenantId: number;
|
||||||
|
viewerUserId: number;
|
||||||
|
viewerName: string;
|
||||||
|
ownerUserId: number;
|
||||||
|
ownerName: string;
|
||||||
|
resourceType: string;
|
||||||
|
enabled: boolean;
|
||||||
|
expireAt?: string;
|
||||||
|
remark?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDataScopeAssignment {
|
||||||
|
tenantId?: number;
|
||||||
|
viewerUserId: number;
|
||||||
|
resourceType?: string;
|
||||||
|
resourceTypes?: string[];
|
||||||
|
ownerUserIds: number[];
|
||||||
|
expireAt?: string;
|
||||||
|
remark?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SysLog {
|
export interface SysLog {
|
||||||
id: number;
|
id: number;
|
||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
alter table if exists crm_opportunity
|
||||||
|
add column if not exists updated_by bigint;
|
||||||
|
|
||||||
|
comment on column crm_opportunity.updated_by is '更新人ID';
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
create table if not exists sys_user_data_scope_user (
|
||||||
|
id bigserial primary key,
|
||||||
|
tenant_id bigint not null,
|
||||||
|
viewer_user_id bigint not null,
|
||||||
|
owner_user_id bigint not null,
|
||||||
|
resource_type varchar(50) not null default 'ALL',
|
||||||
|
enabled boolean not null default true,
|
||||||
|
expire_at timestamptz,
|
||||||
|
remark varchar(500),
|
||||||
|
created_by bigint,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
is_deleted integer not null default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index if not exists uk_user_data_scope_user
|
||||||
|
on sys_user_data_scope_user (tenant_id, viewer_user_id, owner_user_id, resource_type)
|
||||||
|
where is_deleted = 0;
|
||||||
|
|
||||||
|
create index if not exists idx_user_data_scope_viewer
|
||||||
|
on sys_user_data_scope_user (tenant_id, viewer_user_id, resource_type, enabled);
|
||||||
|
|
||||||
|
create index if not exists idx_user_data_scope_owner
|
||||||
|
on sys_user_data_scope_user (tenant_id, owner_user_id);
|
||||||
|
|
||||||
|
comment on table sys_user_data_scope_user is '用户到用户的数据可见范围授权';
|
||||||
|
comment on column sys_user_data_scope_user.viewer_user_id is '被授权查看数据的用户ID';
|
||||||
|
comment on column sys_user_data_scope_user.owner_user_id is '数据归属用户ID';
|
||||||
|
comment on column sys_user_data_scope_user.resource_type is '授权资源类型:OPPORTUNITY/EXPANSION/DAILY_REPORT/CHECKIN等';
|
||||||
|
|
||||||
|
do $$
|
||||||
|
declare
|
||||||
|
v_system_parent_id bigint;
|
||||||
|
v_menu_perm_id bigint;
|
||||||
|
v_view_perm_id bigint;
|
||||||
|
v_update_perm_id bigint;
|
||||||
|
v_has_role_permission_tenant boolean;
|
||||||
|
begin
|
||||||
|
perform setval('sys_permission_perm_id_seq', coalesce((select max(perm_id) from sys_permission), 0) + 1, false);
|
||||||
|
perform setval('sys_role_permission_id_seq', coalesce((select max(id) from sys_role_permission), 0) + 1, false);
|
||||||
|
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = current_schema()
|
||||||
|
and table_name = 'sys_role_permission'
|
||||||
|
and column_name = 'tenant_id'
|
||||||
|
) into v_has_role_permission_tenant;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_system_parent_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'system'
|
||||||
|
and coalesce(is_deleted, 0) = 0
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_system_parent_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
null, '系统管理', 'system', 'directory', 1, null, null, 'SettingOutlined',
|
||||||
|
110, 1, 1, '系统管理目录', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_system_parent_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_menu_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'menu:data-scope-users'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_menu_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_system_parent_id, '数据授权管理', 'menu:data-scope-users', 'menu', 2,
|
||||||
|
'/data-scope-users', null, 'TeamOutlined', 8, 1, 1,
|
||||||
|
'配置用户可额外查看哪些人员名下的数据', jsonb_build_object('tenantScoped', true), 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_menu_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_system_parent_id,
|
||||||
|
name = '数据授权管理',
|
||||||
|
perm_type = 'menu',
|
||||||
|
level = 2,
|
||||||
|
path = '/data-scope-users',
|
||||||
|
component = null,
|
||||||
|
icon = 'TeamOutlined',
|
||||||
|
sort_order = 8,
|
||||||
|
is_visible = 1,
|
||||||
|
status = 1,
|
||||||
|
description = '配置用户可额外查看哪些人员名下的数据',
|
||||||
|
meta = jsonb_build_object('tenantScoped', true),
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_menu_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_view_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'user_data_scope:view'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_view_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_menu_perm_id, '查看数据授权', 'user_data_scope:view', 'button', 3,
|
||||||
|
null, null, null, 1, 1, 1, '查看用户数据授权配置', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_view_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_menu_perm_id,
|
||||||
|
name = '查看数据授权',
|
||||||
|
perm_type = 'button',
|
||||||
|
level = 3,
|
||||||
|
sort_order = 1,
|
||||||
|
is_visible = 1,
|
||||||
|
status = 1,
|
||||||
|
description = '查看用户数据授权配置',
|
||||||
|
meta = '{}'::jsonb,
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_view_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_update_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'user_data_scope:update'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_update_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_menu_perm_id, '修改数据授权', 'user_data_scope:update', 'button', 3,
|
||||||
|
null, null, null, 2, 1, 1, '保存用户数据授权配置', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_update_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_menu_perm_id,
|
||||||
|
name = '修改数据授权',
|
||||||
|
perm_type = 'button',
|
||||||
|
level = 3,
|
||||||
|
sort_order = 2,
|
||||||
|
is_visible = 1,
|
||||||
|
status = 1,
|
||||||
|
description = '保存用户数据授权配置',
|
||||||
|
meta = '{}'::jsonb,
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_update_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_has_role_permission_tenant then
|
||||||
|
insert into sys_role_permission (role_id, perm_id, tenant_id, is_deleted, created_at, updated_at)
|
||||||
|
select role_source.role_id, perm_source.perm_id, role_source.tenant_id, 0, now(), now()
|
||||||
|
from (
|
||||||
|
select distinct role_id, tenant_id
|
||||||
|
from (
|
||||||
|
select r.role_id, r.tenant_id
|
||||||
|
from sys_role r
|
||||||
|
where coalesce(r.is_deleted, 0) = 0
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
)
|
||||||
|
union
|
||||||
|
select r.role_id, r.tenant_id
|
||||||
|
from sys_user u
|
||||||
|
join sys_user_role ur on ur.user_id = u.user_id and coalesce(ur.is_deleted, 0) = 0
|
||||||
|
join sys_role r on r.role_id = ur.role_id and coalesce(r.is_deleted, 0) = 0
|
||||||
|
where coalesce(u.is_deleted, 0) = 0
|
||||||
|
and u.username = 'admin'
|
||||||
|
) granted_roles
|
||||||
|
) role_source
|
||||||
|
cross join (
|
||||||
|
select unnest(array[v_menu_perm_id, v_view_perm_id, v_update_perm_id]) as perm_id
|
||||||
|
) perm_source
|
||||||
|
where perm_source.perm_id is not null
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from sys_role_permission rp
|
||||||
|
where rp.role_id = role_source.role_id
|
||||||
|
and rp.perm_id = perm_source.perm_id
|
||||||
|
);
|
||||||
|
|
||||||
|
update sys_role_permission rp
|
||||||
|
set tenant_id = coalesce(rp.tenant_id, r.tenant_id),
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
from sys_role r,
|
||||||
|
sys_permission p
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and p.perm_id = rp.perm_id
|
||||||
|
and coalesce(r.is_deleted, 0) = 0
|
||||||
|
and p.code in ('menu:data-scope-users', 'user_data_scope:view', 'user_data_scope:update')
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from sys_user u
|
||||||
|
join sys_user_role ur on ur.user_id = u.user_id and coalesce(ur.is_deleted, 0) = 0
|
||||||
|
where coalesce(u.is_deleted, 0) = 0
|
||||||
|
and u.username = 'admin'
|
||||||
|
and ur.role_id = r.role_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
insert into sys_role_permission (role_id, perm_id, is_deleted, created_at, updated_at)
|
||||||
|
select role_source.role_id, perm_source.perm_id, 0, now(), now()
|
||||||
|
from (
|
||||||
|
select distinct role_id
|
||||||
|
from (
|
||||||
|
select r.role_id
|
||||||
|
from sys_role r
|
||||||
|
where coalesce(r.is_deleted, 0) = 0
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
)
|
||||||
|
union
|
||||||
|
select r.role_id
|
||||||
|
from sys_user u
|
||||||
|
join sys_user_role ur on ur.user_id = u.user_id and coalesce(ur.is_deleted, 0) = 0
|
||||||
|
join sys_role r on r.role_id = ur.role_id and coalesce(r.is_deleted, 0) = 0
|
||||||
|
where coalesce(u.is_deleted, 0) = 0
|
||||||
|
and u.username = 'admin'
|
||||||
|
) granted_roles
|
||||||
|
) role_source
|
||||||
|
cross join (
|
||||||
|
select unnest(array[v_menu_perm_id, v_view_perm_id, v_update_perm_id]) as perm_id
|
||||||
|
) perm_source
|
||||||
|
where perm_source.perm_id is not null
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from sys_role_permission rp
|
||||||
|
where rp.role_id = role_source.role_id
|
||||||
|
and rp.perm_id = perm_source.perm_id
|
||||||
|
);
|
||||||
|
|
||||||
|
update sys_role_permission rp
|
||||||
|
set is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
from sys_role r,
|
||||||
|
sys_permission p
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and p.perm_id = rp.perm_id
|
||||||
|
and coalesce(r.is_deleted, 0) = 0
|
||||||
|
and p.code in ('menu:data-scope-users', 'user_data_scope:view', 'user_data_scope:update')
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from sys_user u
|
||||||
|
join sys_user_role ur on ur.user_id = u.user_id and coalesce(ur.is_deleted, 0) = 0
|
||||||
|
where coalesce(u.is_deleted, 0) = 0
|
||||||
|
and u.username = 'admin'
|
||||||
|
and ur.role_id = r.role_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
-- 20260701
|
||||||
|
-- 新增工作日报按钮权限点:
|
||||||
|
-- daily_report:notify_users 选择通知用户
|
||||||
|
-- daily_report:attachments 日报附件
|
||||||
|
-- 执行后可在 frontend1 权限管理与角色权限绑定中维护这些权限。
|
||||||
|
-- 可重复执行;CRM业务与工作节点仅用于权限管理,不在后台侧边栏显示。
|
||||||
|
-- “# 关联对象是否必填”继续由 application.yml 的 work-report.daily-report 配置控制。
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to public;
|
||||||
|
|
||||||
|
do $$
|
||||||
|
declare
|
||||||
|
v_business_parent_id bigint;
|
||||||
|
v_work_menu_perm_id bigint;
|
||||||
|
v_notify_users_perm_id bigint;
|
||||||
|
v_attachments_perm_id bigint;
|
||||||
|
v_has_role_permission_tenant boolean;
|
||||||
|
begin
|
||||||
|
perform setval('sys_permission_perm_id_seq', coalesce((select max(perm_id) from sys_permission), 0) + 1, false);
|
||||||
|
perform setval('sys_role_permission_id_seq', coalesce((select max(id) from sys_role_permission), 0) + 1, false);
|
||||||
|
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = current_schema()
|
||||||
|
and table_name = 'sys_role_permission'
|
||||||
|
and column_name = 'tenant_id'
|
||||||
|
) into v_has_role_permission_tenant;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_business_parent_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'crm'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_business_parent_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
null, 'CRM业务', 'crm', 'directory', 1, null, null, 'AppstoreOutlined',
|
||||||
|
10, 0, 1, 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_business_parent_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set name = 'CRM业务',
|
||||||
|
perm_type = 'directory',
|
||||||
|
level = 1,
|
||||||
|
path = null,
|
||||||
|
component = null,
|
||||||
|
icon = coalesce(nullif(icon, ''), 'AppstoreOutlined'),
|
||||||
|
sort_order = coalesce(sort_order, 10),
|
||||||
|
is_visible = 0,
|
||||||
|
status = 1,
|
||||||
|
description = 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示',
|
||||||
|
meta = coalesce(meta, '{}'::jsonb),
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_business_parent_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_work_menu_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'menu:work'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_work_menu_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_business_parent_id, '工作', 'menu:work', 'menu', 2,
|
||||||
|
null, null, 'ScheduleOutlined', 40, 0, 1,
|
||||||
|
'CRM前台工作台与日报权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_work_menu_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_business_parent_id,
|
||||||
|
name = '工作',
|
||||||
|
perm_type = 'menu',
|
||||||
|
level = 2,
|
||||||
|
path = null,
|
||||||
|
component = null,
|
||||||
|
icon = coalesce(nullif(icon, ''), 'ScheduleOutlined'),
|
||||||
|
sort_order = coalesce(sort_order, 40),
|
||||||
|
is_visible = 0,
|
||||||
|
status = 1,
|
||||||
|
description = 'CRM前台工作台与日报权限分组,仅用于权限管理,不在后台侧边栏显示',
|
||||||
|
meta = coalesce(meta, '{}'::jsonb),
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_work_menu_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_notify_users_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'daily_report:notify_users'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_notify_users_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_work_menu_perm_id, '选择通知用户', 'daily_report:notify_users', 'button', 3,
|
||||||
|
null, null, null, 1, 1, 1, '控制CRM前台日报中的选择通知用户按钮和用户选择接口', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_notify_users_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_work_menu_perm_id,
|
||||||
|
name = '选择通知用户',
|
||||||
|
perm_type = 'button',
|
||||||
|
level = 3,
|
||||||
|
path = null,
|
||||||
|
component = null,
|
||||||
|
icon = null,
|
||||||
|
sort_order = 1,
|
||||||
|
is_visible = 1,
|
||||||
|
status = 1,
|
||||||
|
description = '控制CRM前台日报中的选择通知用户按钮和用户选择接口',
|
||||||
|
meta = '{}'::jsonb,
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_notify_users_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select perm_id
|
||||||
|
into v_attachments_perm_id
|
||||||
|
from sys_permission
|
||||||
|
where code = 'daily_report:attachments'
|
||||||
|
order by perm_id
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_attachments_perm_id is null then
|
||||||
|
insert into sys_permission (
|
||||||
|
parent_id, name, code, perm_type, level, path, component, icon,
|
||||||
|
sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at
|
||||||
|
) values (
|
||||||
|
v_work_menu_perm_id, '日报附件', 'daily_report:attachments', 'button', 3,
|
||||||
|
null, null, null, 2, 1, 1, '控制CRM前台日报附件按钮、附件上传接口和日报附件保存', '{}'::jsonb, 0, now(), now()
|
||||||
|
)
|
||||||
|
returning perm_id into v_attachments_perm_id;
|
||||||
|
else
|
||||||
|
update sys_permission
|
||||||
|
set parent_id = v_work_menu_perm_id,
|
||||||
|
name = '日报附件',
|
||||||
|
perm_type = 'button',
|
||||||
|
level = 3,
|
||||||
|
path = null,
|
||||||
|
component = null,
|
||||||
|
icon = null,
|
||||||
|
sort_order = 2,
|
||||||
|
is_visible = 1,
|
||||||
|
status = 1,
|
||||||
|
description = '控制CRM前台日报附件按钮、附件上传接口和日报附件保存',
|
||||||
|
meta = '{}'::jsonb,
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
where perm_id = v_attachments_perm_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_has_role_permission_tenant then
|
||||||
|
insert into sys_role_permission (role_id, perm_id, tenant_id, is_deleted, created_at, updated_at)
|
||||||
|
select distinct r.role_id, permission_source.perm_id, r.tenant_id, 0, now(), now()
|
||||||
|
from sys_role r
|
||||||
|
cross join (
|
||||||
|
select unnest(array[v_business_parent_id, v_work_menu_perm_id, v_notify_users_perm_id, v_attachments_perm_id]) as perm_id
|
||||||
|
) permission_source
|
||||||
|
where coalesce(r.is_deleted, 0) = 0
|
||||||
|
and permission_source.perm_id is not null
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
)
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from sys_role_permission rp
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and rp.perm_id = permission_source.perm_id
|
||||||
|
);
|
||||||
|
|
||||||
|
update sys_role_permission rp
|
||||||
|
set tenant_id = coalesce(rp.tenant_id, r.tenant_id),
|
||||||
|
is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
from sys_role r
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and rp.perm_id in (v_business_parent_id, v_work_menu_perm_id, v_notify_users_perm_id, v_attachments_perm_id)
|
||||||
|
and coalesce(r.is_deleted, 0) = 0
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
);
|
||||||
|
else
|
||||||
|
insert into sys_role_permission (role_id, perm_id, is_deleted, created_at, updated_at)
|
||||||
|
select distinct r.role_id, permission_source.perm_id, 0, now(), now()
|
||||||
|
from sys_role r
|
||||||
|
cross join (
|
||||||
|
select unnest(array[v_business_parent_id, v_work_menu_perm_id, v_notify_users_perm_id, v_attachments_perm_id]) as perm_id
|
||||||
|
) permission_source
|
||||||
|
where coalesce(r.is_deleted, 0) = 0
|
||||||
|
and permission_source.perm_id is not null
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
)
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from sys_role_permission rp
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and rp.perm_id = permission_source.perm_id
|
||||||
|
);
|
||||||
|
|
||||||
|
update sys_role_permission rp
|
||||||
|
set is_deleted = 0,
|
||||||
|
updated_at = now()
|
||||||
|
from sys_role r
|
||||||
|
where rp.role_id = r.role_id
|
||||||
|
and rp.perm_id in (v_business_parent_id, v_work_menu_perm_id, v_notify_users_perm_id, v_attachments_perm_id)
|
||||||
|
and coalesce(r.is_deleted, 0) = 0
|
||||||
|
and (
|
||||||
|
r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN')
|
||||||
|
or r.role_name ilike '%管理员%'
|
||||||
|
or r.role_name ilike '%admin%'
|
||||||
|
);
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
@ -141,6 +141,7 @@ create table if not exists crm_opportunity (
|
||||||
competitor_name varchar(200),
|
competitor_name varchar(200),
|
||||||
latest_progress text,
|
latest_progress text,
|
||||||
next_plan text,
|
next_plan text,
|
||||||
|
updated_by bigint,
|
||||||
archived boolean not null default false,
|
archived boolean not null default false,
|
||||||
archived_at timestamptz,
|
archived_at timestamptz,
|
||||||
pushed_to_oms boolean not null default false,
|
pushed_to_oms boolean not null default false,
|
||||||
|
|
@ -861,6 +862,7 @@ WITH column_comments(table_name, column_name, comment_text) AS (
|
||||||
('crm_opportunity', 'competitor_name', '竞品名称'),
|
('crm_opportunity', 'competitor_name', '竞品名称'),
|
||||||
('crm_opportunity', 'latest_progress', '项目最新进展'),
|
('crm_opportunity', 'latest_progress', '项目最新进展'),
|
||||||
('crm_opportunity', 'next_plan', '下一步销售计划'),
|
('crm_opportunity', 'next_plan', '下一步销售计划'),
|
||||||
|
('crm_opportunity', 'updated_by', '更新人ID'),
|
||||||
('crm_opportunity', 'archived', '是否归档'),
|
('crm_opportunity', 'archived', '是否归档'),
|
||||||
('crm_opportunity', 'archived_at', '归档时间'),
|
('crm_opportunity', 'archived_at', '归档时间'),
|
||||||
('crm_opportunity', 'pushed_to_oms', '是否已推送OMS'),
|
('crm_opportunity', 'pushed_to_oms', '是否已推送OMS'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue