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 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 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 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("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.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.actual_signed_amount is '实际签约金额'");
|
||||
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 nextAction;
|
||||
private String user;
|
||||
private String roleCodes;
|
||||
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
||||
|
||||
public Long getId() {
|
||||
|
|
@ -98,6 +99,14 @@ public class OpportunityFollowUpDTO {
|
|||
this.user = user;
|
||||
}
|
||||
|
||||
public String getRoleCodes() {
|
||||
return roleCodes;
|
||||
}
|
||||
|
||||
public void setRoleCodes(String roleCodes) {
|
||||
this.roleCodes = roleCodes;
|
||||
}
|
||||
|
||||
public List<WorkReportAttachmentDTO> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.unis.crm.dto.work;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WorkCheckInExportDTO {
|
||||
|
||||
private Long id;
|
||||
private String checkinDate;
|
||||
private String checkinTime;
|
||||
private String userName;
|
||||
|
|
@ -19,8 +21,17 @@ public class WorkCheckInExportDTO {
|
|||
private String status;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private OffsetDateTime sortTime;
|
||||
private List<String> photoUrls = new ArrayList<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCheckinDate() {
|
||||
return checkinDate;
|
||||
}
|
||||
|
|
@ -125,6 +136,14 @@ public class WorkCheckInExportDTO {
|
|||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public OffsetDateTime getSortTime() {
|
||||
return sortTime;
|
||||
}
|
||||
|
||||
public void setSortTime(OffsetDateTime sortTime) {
|
||||
this.sortTime = sortTime;
|
||||
}
|
||||
|
||||
public List<String> getPhotoUrls() {
|
||||
return photoUrls;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package com.unis.crm.dto.work;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class WorkDailyReportExportDTO {
|
||||
|
||||
private Long id;
|
||||
private String reportDate;
|
||||
private String submitTime;
|
||||
private String userName;
|
||||
|
|
@ -19,10 +21,19 @@ public class WorkDailyReportExportDTO {
|
|||
private String reviewedAt;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private OffsetDateTime sortTime;
|
||||
private List<WorkReportLineItemDTO> lineItems = new ArrayList<>();
|
||||
private List<WorkReportAttachmentDTO> attachments = new ArrayList<>();
|
||||
private List<WorkTomorrowPlanItemDTO> planItems = new ArrayList<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getReportDate() {
|
||||
return reportDate;
|
||||
}
|
||||
|
|
@ -135,6 +146,14 @@ public class WorkDailyReportExportDTO {
|
|||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public OffsetDateTime getSortTime() {
|
||||
return sortTime;
|
||||
}
|
||||
|
||||
public void setSortTime(OffsetDateTime sortTime) {
|
||||
this.sortTime = sortTime;
|
||||
}
|
||||
|
||||
public List<WorkReportLineItemDTO> getLineItems() {
|
||||
return lineItems;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,17 @@ public interface ExpansionMapper {
|
|||
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
|
||||
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")
|
||||
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(
|
||||
@Param("tenantId") Long tenantId,
|
||||
@Param("keyword") String keyword,
|
||||
|
|
@ -45,19 +53,14 @@ public interface ExpansionMapper {
|
|||
@Param("keyword") String keyword,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
|
||||
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);
|
||||
|
||||
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
|
||||
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);
|
||||
|
||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
||||
List<ChannelRelatedProjectSummaryDTO> selectChannelRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
|
||||
|
||||
int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request);
|
||||
|
|
|
|||
|
|
@ -32,11 +32,23 @@ public interface WorkMapper {
|
|||
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
||||
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")
|
||||
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 selectReportHistoryByIdForUserIds(
|
||||
@Param("reportId") Long reportId,
|
||||
@Param("visibleUserIds") List<Long> visibleUserIds);
|
||||
|
||||
@DataScope(tableAlias = "c", ownerColumn = "user_id")
|
||||
List<WorkCheckInExportDTO> selectCheckInExportRows(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
|
|
@ -47,6 +59,16 @@ public interface WorkMapper {
|
|||
@Param("status") String status,
|
||||
@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")
|
||||
List<WorkDailyReportExportDTO> selectDailyReportExportRows(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
|
|
@ -56,6 +78,15 @@ public interface WorkMapper {
|
|||
@Param("status") String status,
|
||||
@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);
|
||||
|
||||
int insertCheckIn(@Param("userId") Long userId, @Param("request") CreateWorkCheckInRequest request);
|
||||
|
|
@ -164,7 +195,8 @@ public interface WorkMapper {
|
|||
@Param("opportunityId") Long opportunityId,
|
||||
@Param("latestProgress") String latestProgress,
|
||||
@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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.UpdateSalesExpansionRequest;
|
||||
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.unisbase.security.PermissionService;
|
||||
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||
|
|
@ -57,14 +59,17 @@ public class ExpansionServiceImpl implements ExpansionService {
|
|||
private final ExpansionMapper expansionMapper;
|
||||
private final SpringSecurityTenantProvider tenantProvider;
|
||||
private final PermissionService permissionService;
|
||||
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||
|
||||
public ExpansionServiceImpl(
|
||||
ExpansionMapper expansionMapper,
|
||||
SpringSecurityTenantProvider tenantProvider,
|
||||
PermissionService permissionService) {
|
||||
PermissionService permissionService,
|
||||
CrmDataVisibilityService crmDataVisibilityService) {
|
||||
this.expansionMapper = expansionMapper;
|
||||
this.tenantProvider = tenantProvider;
|
||||
this.permissionService = permissionService;
|
||||
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -102,8 +107,17 @@ public class ExpansionServiceImpl implements ExpansionService {
|
|||
@Override
|
||||
public ExpansionOverviewDTO getOverview(Long userId, String keyword) {
|
||||
String normalizedKeyword = normalizeKeyword(keyword);
|
||||
List<SalesExpansionItemDTO> salesItems = expansionMapper.selectSalesExpansions(userId, normalizedKeyword);
|
||||
List<ChannelExpansionItemDTO> channelItems = expansionMapper.selectChannelExpansions(userId, normalizedKeyword);
|
||||
List<Long> extraVisibleOwnerUserIds = resolveExtraVisibleOwnerUserIds(userId);
|
||||
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);
|
||||
attachSalesRelatedProjects(userId, salesItems);
|
||||
|
|
@ -115,6 +129,64 @@ public class ExpansionServiceImpl implements ExpansionService {
|
|||
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
|
||||
public ExpansionOverviewDTO getOpportunityFormOptions(Long userId, String keyword, Integer limit) {
|
||||
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.work.WorkReportAttachmentDTO;
|
||||
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.OpportunityService;
|
||||
import com.unisbase.dto.DataScopeRuleDTO;
|
||||
import com.unisbase.security.PermissionService;
|
||||
import com.unisbase.service.DataScopeService;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -63,7 +63,7 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
private final OpportunityMapper opportunityMapper;
|
||||
private final OmsClient omsClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DataScopeService dataScopeService;
|
||||
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||
private final UnisBaseTenantProvider tenantProvider;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
|
|
@ -71,13 +71,13 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
OpportunityMapper opportunityMapper,
|
||||
OmsClient omsClient,
|
||||
ObjectMapper objectMapper,
|
||||
DataScopeService dataScopeService,
|
||||
CrmDataVisibilityService crmDataVisibilityService,
|
||||
UnisBaseTenantProvider tenantProvider,
|
||||
PermissionService permissionService) {
|
||||
this.opportunityMapper = opportunityMapper;
|
||||
this.omsClient = omsClient;
|
||||
this.objectMapper = objectMapper;
|
||||
this.dataScopeService = dataScopeService;
|
||||
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||
this.tenantProvider = tenantProvider;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
|
@ -318,18 +318,19 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
}
|
||||
List<String> preSalesUserNames = resolveCurrentUserPreSalesNames(userId);
|
||||
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);
|
||||
}
|
||||
|
||||
DataScopeRuleDTO rule = dataScopeService.resolveUserScope(userId, tenantId);
|
||||
if (rule == null) {
|
||||
return new OpportunityVisibility(false, List.of(userId), userId, preSalesUserNames);
|
||||
}
|
||||
if (rule.isAllAccess()) {
|
||||
return new OpportunityVisibility(true, List.of(), userId, preSalesUserNames);
|
||||
}
|
||||
return new OpportunityVisibility(false, normalizeVisibleOwnerUserIds(rule.getCreatorUserIds()), userId, preSalesUserNames);
|
||||
DataVisibility visibility = crmDataVisibilityService.resolveVisibility(
|
||||
userId,
|
||||
tenantId,
|
||||
CrmDataVisibilityService.RESOURCE_OPPORTUNITY);
|
||||
return new OpportunityVisibility(
|
||||
visibility.allDataAccess(),
|
||||
normalizeVisibleOwnerUserIds(visibility.visibleOwnerUserIds()),
|
||||
userId,
|
||||
preSalesUserNames);
|
||||
}
|
||||
|
||||
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.ProfileMapper;
|
||||
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.WorkService;
|
||||
import com.unisbase.service.SysPermissionService;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -49,9 +52,11 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
|
|
@ -59,6 +64,7 @@ import java.util.regex.Pattern;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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 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 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 LEGACY_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 ObjectMapper objectMapper;
|
||||
private final UnisBaseTenantProvider tenantProvider;
|
||||
private final CrmDataVisibilityService crmDataVisibilityService;
|
||||
private final ReportReminderService reportReminderService;
|
||||
private final WorkReportProperties workReportProperties;
|
||||
private final SysPermissionService sysPermissionService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final Path checkInPhotoDirectory;
|
||||
private final Path reportAttachmentDirectory;
|
||||
private final String tencentMapKey;
|
||||
|
|
@ -120,6 +132,9 @@ public class WorkServiceImpl implements WorkService {
|
|||
WorkReportProperties workReportProperties,
|
||||
ObjectMapper objectMapper,
|
||||
UnisBaseTenantProvider tenantProvider,
|
||||
CrmDataVisibilityService crmDataVisibilityService,
|
||||
SysPermissionService sysPermissionService,
|
||||
JdbcTemplate jdbcTemplate,
|
||||
@Value("${unisbase.app.upload-path}") String uploadPath,
|
||||
@Value("${unisbase.app.tencent-map.key:}") String tencentMapKey) {
|
||||
this.workMapper = workMapper;
|
||||
|
|
@ -129,6 +144,9 @@ public class WorkServiceImpl implements WorkService {
|
|||
this.workReportProperties = workReportProperties == null ? new WorkReportProperties() : workReportProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.tenantProvider = tenantProvider;
|
||||
this.crmDataVisibilityService = crmDataVisibilityService;
|
||||
this.sysPermissionService = sysPermissionService;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(8))
|
||||
.build();
|
||||
|
|
@ -151,7 +169,7 @@ public class WorkServiceImpl implements WorkService {
|
|||
@Override
|
||||
public List<WorkReportToUserDTO> listReportMessageUsers(Long userId, String keyword) {
|
||||
requireUser(userId);
|
||||
if (!isDailyReportNotifyUsersVisible(loadUserRoleCodes(userId))) {
|
||||
if (!isDailyReportNotifyUsersVisible(userId, loadUserRoleCodes(userId))) {
|
||||
throw new BusinessException("当前角色不可使用日报通知用户功能");
|
||||
}
|
||||
return workMapper.selectReportMessageUsers(resolveCurrentTenantId(), normalizeOptionalText(keyword), 50);
|
||||
|
|
@ -163,8 +181,8 @@ public class WorkServiceImpl implements WorkService {
|
|||
List<String> roleCodes = loadUserRoleCodes(userId);
|
||||
return new WorkDailyReportRequirementDTO(
|
||||
isDailyReportObjectSelectionRequired(roleCodes),
|
||||
isDailyReportNotifyUsersVisible(roleCodes),
|
||||
isDailyReportAttachmentsVisible(roleCodes));
|
||||
isDailyReportNotifyUsersVisible(userId, roleCodes),
|
||||
isDailyReportAttachmentsVisible(userId, roleCodes));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -175,7 +193,7 @@ public class WorkServiceImpl implements WorkService {
|
|||
int offset = (safePage - 1) * safeSize;
|
||||
HistoryType historyType = mapHistoryType(type);
|
||||
int fetchLimit = offset + safeSize + 1;
|
||||
List<WorkHistoryItemDTO> historyItems = loadHistoryItems(historyType, fetchLimit);
|
||||
List<WorkHistoryItemDTO> historyItems = loadHistoryItems(userId, historyType, fetchLimit);
|
||||
|
||||
boolean hasMore = historyItems.size() > offset + safeSize;
|
||||
if (offset >= historyItems.size()) {
|
||||
|
|
@ -195,6 +213,12 @@ public class WorkServiceImpl implements WorkService {
|
|||
throw new BusinessException("日报不存在");
|
||||
}
|
||||
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) {
|
||||
throw new BusinessException("日报不存在或无权查看");
|
||||
}
|
||||
|
|
@ -214,14 +238,29 @@ public class WorkServiceImpl implements WorkService {
|
|||
requireUser(userId);
|
||||
validateDateRange(startDate, endDate);
|
||||
String normalizedBizType = normalizeBizType(bizType);
|
||||
List<WorkCheckInExportDTO> rows = workMapper.selectCheckInExportRows(
|
||||
startDate,
|
||||
endDate,
|
||||
normalizeOptionalText(keyword),
|
||||
normalizeOptionalText(deptName),
|
||||
normalizedBizType,
|
||||
normalizeOptionalText(status),
|
||||
EXPORT_LIMIT);
|
||||
List<Long> extraVisibleUserIds = resolveExtraVisibleUserIds(
|
||||
userId,
|
||||
CrmDataVisibilityService.RESOURCE_CHECKIN);
|
||||
List<WorkCheckInExportDTO> rows = limitMergedWorkCheckInExportRows(
|
||||
workMapper.selectCheckInExportRows(
|
||||
startDate,
|
||||
endDate,
|
||||
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);
|
||||
return rows;
|
||||
}
|
||||
|
|
@ -238,13 +277,27 @@ public class WorkServiceImpl implements WorkService {
|
|||
requireUser(userId);
|
||||
validateDateRange(startDate, endDate);
|
||||
String normalizedBizType = normalizeBizType(bizType);
|
||||
List<WorkDailyReportExportDTO> rows = workMapper.selectDailyReportExportRows(
|
||||
startDate,
|
||||
endDate,
|
||||
normalizeOptionalText(keyword),
|
||||
normalizeOptionalText(deptName),
|
||||
normalizeOptionalText(status),
|
||||
EXPORT_LIMIT);
|
||||
List<Long> extraVisibleUserIds = resolveExtraVisibleUserIds(
|
||||
userId,
|
||||
CrmDataVisibilityService.RESOURCE_DAILY_REPORT);
|
||||
List<WorkDailyReportExportDTO> rows = limitMergedWorkDailyReportExportRows(
|
||||
workMapper.selectDailyReportExportRows(
|
||||
startDate,
|
||||
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);
|
||||
if (normalizedBizType == null) {
|
||||
return rows;
|
||||
|
|
@ -294,8 +347,8 @@ public class WorkServiceImpl implements WorkService {
|
|||
normalizeDailyReportRequest(
|
||||
request,
|
||||
isDailyReportObjectSelectionRequired(roleCodes),
|
||||
isDailyReportNotifyUsersVisible(roleCodes),
|
||||
isDailyReportAttachmentsVisible(roleCodes));
|
||||
isDailyReportNotifyUsersVisible(userId, roleCodes),
|
||||
isDailyReportAttachmentsVisible(userId, roleCodes));
|
||||
request.setSourceType(normalizeOptionalText(request.getSourceType()));
|
||||
if (request.getSourceType() == null) {
|
||||
request.setSourceType("manual");
|
||||
|
|
@ -327,7 +380,8 @@ public class WorkServiceImpl implements WorkService {
|
|||
reportId,
|
||||
previousReport == null ? List.of() : previousReport.getLineItems(),
|
||||
request,
|
||||
currentReport);
|
||||
currentReport,
|
||||
isSalesRole(roleCodes));
|
||||
syncTomorrowPlanTodo(userId, reportId, request.getPlanItems());
|
||||
syncReportMessages(userId, reportId, resolveReportDate(currentReport), request);
|
||||
Long finalReportId = reportId;
|
||||
|
|
@ -427,7 +481,7 @@ public class WorkServiceImpl implements WorkService {
|
|||
public WorkReportAttachmentDTO uploadReportAttachment(Long userId, MultipartFile file) {
|
||||
requireUser(userId);
|
||||
List<String> roleCodes = ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
|
||||
if (!isDailyReportAttachmentsVisible(roleCodes)) {
|
||||
if (!isDailyReportAttachmentsVisible(userId, roleCodes)) {
|
||||
throw new BusinessException("当前角色不可使用日报附件功能");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
|
|
@ -528,12 +582,24 @@ public class WorkServiceImpl implements WorkService {
|
|||
return dailyReport.isRequiredByDefault();
|
||||
}
|
||||
|
||||
private boolean isDailyReportNotifyUsersVisible(List<String> roleCodes) {
|
||||
return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getNotifyUsers());
|
||||
private boolean isDailyReportNotifyUsersVisible(Long userId, List<String> roleCodes) {
|
||||
return isPermissionControlledFeatureVisible(
|
||||
userId,
|
||||
DAILY_REPORT_NOTIFY_USERS_PERMISSION,
|
||||
roleCodes,
|
||||
workReportProperties.getDailyReport().getNotifyUsers());
|
||||
}
|
||||
|
||||
private boolean isDailyReportAttachmentsVisible(List<String> roleCodes) {
|
||||
return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getAttachments());
|
||||
private boolean isDailyReportAttachmentsVisible(Long userId, List<String> roleCodes) {
|
||||
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) {
|
||||
|
|
@ -549,6 +615,45 @@ public class WorkServiceImpl implements WorkService {
|
|||
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) {
|
||||
if (userRoleCodes == null || userRoleCodes.isEmpty() || configuredRoleCodes == null || configuredRoleCodes.isEmpty()) {
|
||||
return false;
|
||||
|
|
@ -568,18 +673,132 @@ public class WorkServiceImpl implements WorkService {
|
|||
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<>();
|
||||
if (historyType == null || historyType == HistoryType.CHECK_IN) {
|
||||
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) {
|
||||
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());
|
||||
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) {
|
||||
String normalizedType = normalizeOptionalText(type);
|
||||
if (normalizedType == null) {
|
||||
|
|
@ -1456,7 +1675,8 @@ public class WorkServiceImpl implements WorkService {
|
|||
Long reportId,
|
||||
List<WorkReportLineItemDTO> previousLineItems,
|
||||
CreateWorkDailyReportRequest request,
|
||||
WorkDailyReportDTO currentReport) {
|
||||
WorkDailyReportDTO currentReport,
|
||||
boolean syncOpportunitySnapshot) {
|
||||
LocalDate reportDate = resolveReportDate(currentReport);
|
||||
|
||||
for (WorkReportLineItemDTO item : previousLineItems) {
|
||||
|
|
@ -1498,11 +1718,14 @@ public class WorkServiceImpl implements WorkService {
|
|||
WORK_REPORT_FOLLOW_UP_TYPE,
|
||||
appendReportAttachmentMetadata(item.getContent(), item.getAttachments()),
|
||||
resolveOpportunityNextAction(item));
|
||||
workMapper.updateOpportunitySnapshot(
|
||||
item.getBizId(),
|
||||
normalizeSnapshotText(item.getLatestProgress()),
|
||||
normalizeSnapshotText(item.getNextPlan()),
|
||||
item.getStage());
|
||||
if (syncOpportunitySnapshot) {
|
||||
workMapper.updateOpportunitySnapshot(
|
||||
item.getBizId(),
|
||||
normalizeSnapshotText(item.getLatestProgress()),
|
||||
normalizeSnapshotText(item.getNextPlan()),
|
||||
item.getStage(),
|
||||
userId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
workMapper.deleteDailyReportExpansionFollowUps(
|
||||
|
|
|
|||
|
|
@ -89,17 +89,3 @@ unisbase:
|
|||
required-role-codes: [crm_xs]
|
||||
# 允许不使用 #,可直接填写文字日报的角色编码。
|
||||
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]
|
||||
# 允许不使用 #,可直接填写文字日报的角色编码。
|
||||
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">
|
||||
<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
|
||||
item_label as label,
|
||||
|
|
@ -136,6 +361,19 @@
|
|||
order by s.updated_at desc, s.id desc
|
||||
</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
|
||||
c.id,
|
||||
|
|
@ -271,6 +509,19 @@
|
|||
order by c.updated_at desc, c.id desc
|
||||
</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
|
||||
s.id,
|
||||
|
|
|
|||
|
|
@ -377,10 +377,21 @@
|
|||
coalesce(f.followup_type, '无') as type,
|
||||
coalesce(f.content, '无') as content,
|
||||
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
|
||||
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 (
|
||||
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
|
||||
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
|
|
@ -448,6 +459,7 @@
|
|||
competitor_name,
|
||||
latest_progress,
|
||||
next_plan,
|
||||
updated_by,
|
||||
pushed_to_oms,
|
||||
oms_push_time,
|
||||
description,
|
||||
|
|
@ -473,6 +485,7 @@
|
|||
#{request.competitorName},
|
||||
#{request.latestProgress},
|
||||
#{request.nextPlan},
|
||||
#{userId},
|
||||
false,
|
||||
null,
|
||||
#{request.description},
|
||||
|
|
@ -509,6 +522,7 @@
|
|||
<update id="updateOpportunityCode">
|
||||
update crm_opportunity o
|
||||
set opportunity_code = #{opportunityCode},
|
||||
updated_by = #{userId},
|
||||
updated_at = now()
|
||||
where o.id = #{opportunityId}
|
||||
</update>
|
||||
|
|
@ -575,6 +589,7 @@
|
|||
update crm_opportunity o
|
||||
set pre_sales_id = #{preSalesId},
|
||||
pre_sales_name = #{preSalesName},
|
||||
updated_by = #{userId},
|
||||
updated_at = now()
|
||||
where o.id = #{opportunityId}
|
||||
</update>
|
||||
|
|
@ -597,6 +612,7 @@
|
|||
competitor_name = #{request.competitorName},
|
||||
latest_progress = #{request.latestProgress},
|
||||
next_plan = #{request.nextPlan},
|
||||
updated_by = #{userId},
|
||||
description = #{request.description},
|
||||
status = case
|
||||
when #{request.stage} = 'won' then 'won'
|
||||
|
|
@ -711,6 +727,7 @@
|
|||
set pushed_to_oms = true,
|
||||
oms_push_time = now(),
|
||||
opportunity_code = #{opportunityCode},
|
||||
updated_by = #{userId},
|
||||
updated_at = now()
|
||||
where o.id = #{opportunityId}
|
||||
</update>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,58 @@
|
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<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,
|
||||
|
|
@ -228,6 +280,42 @@
|
|||
limit #{limit}
|
||||
</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
|
||||
r.id,
|
||||
|
|
@ -270,6 +358,52 @@
|
|||
limit #{limit}
|
||||
</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
|
||||
r.id,
|
||||
|
|
@ -321,8 +455,55 @@
|
|||
limit 1
|
||||
</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
|
||||
c.id,
|
||||
to_char(c.checkin_date, 'YYYY-MM-DD') as checkinDate,
|
||||
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,
|
||||
|
|
@ -335,7 +516,8 @@
|
|||
coalesce(c.remark, '') as remark,
|
||||
coalesce(c.status, 'normal') as status,
|
||||
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
|
||||
left join sys_user u on u.user_id = c.user_id and u.is_deleted = 0
|
||||
left join lateral (
|
||||
|
|
@ -347,37 +529,50 @@
|
|||
and o.is_deleted = 0
|
||||
) org_info on true
|
||||
where 1 = 1
|
||||
<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>
|
||||
<include refid="checkInExportFilters"/>
|
||||
order by coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) desc nulls last, c.id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectCheckInExportRowsByUserIds" resultType="com.unis.crm.dto.work.WorkCheckInExportDTO">
|
||||
select
|
||||
c.id,
|
||||
to_char(c.checkin_date, 'YYYY-MM-DD') as checkinDate,
|
||||
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.dept_name), ''), nullif(btrim(org_info.org_names), ''), '') as deptName,
|
||||
coalesce(c.biz_type, '') as bizType,
|
||||
coalesce(c.biz_name, '') as bizName,
|
||||
coalesce(c.location_text, '') as locationText,
|
||||
c.longitude,
|
||||
c.latitude,
|
||||
coalesce(c.remark, '') as remark,
|
||||
coalesce(c.status, 'normal') as status,
|
||||
to_char(c.created_at, 'YYYY-MM-DD HH24:MI') as createdAt,
|
||||
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
|
||||
left join sys_user u on u.user_id = c.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 = 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
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectDailyReportExportRows" resultType="com.unis.crm.dto.work.WorkDailyReportExportDTO">
|
||||
select
|
||||
r.id,
|
||||
to_char(r.report_date, 'YYYY-MM-DD') as reportDate,
|
||||
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
|
||||
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,
|
||||
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.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
|
||||
left join sys_user u on u.user_id = r.user_id and u.is_deleted = 0
|
||||
left join lateral (
|
||||
|
|
@ -414,27 +610,55 @@
|
|||
) 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 1 = 1
|
||||
<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>
|
||||
<include refid="dailyReportExportFilters"/>
|
||||
order by coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) desc nulls last, r.id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectDailyReportExportRowsByUserIds" resultType="com.unis.crm.dto.work.WorkDailyReportExportDTO">
|
||||
select
|
||||
r.id,
|
||||
to_char(r.report_date, 'YYYY-MM-DD') as reportDate,
|
||||
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(org_info.org_names), ''), '') as deptName,
|
||||
coalesce(r.source_type, 'manual') as sourceType,
|
||||
coalesce(r.status, 'submitted') as status,
|
||||
coalesce(r.work_content, '') as workContent,
|
||||
coalesce(r.tomorrow_plan, '') as tomorrowPlan,
|
||||
rc.score,
|
||||
rc.comment_content as comment,
|
||||
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(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
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
|
@ -752,6 +976,7 @@
|
|||
set latest_progress = #{latestProgress},
|
||||
next_plan = #{nextPlan},
|
||||
stage = coalesce(#{stage}, stage),
|
||||
updated_by = #{updatedBy},
|
||||
updated_at = now()
|
||||
where id = #{opportunityId}
|
||||
</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.UpdateSalesExpansionRequest;
|
||||
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.SpringSecurityTenantProvider;
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -41,9 +43,34 @@ class ExpansionServiceImplTest {
|
|||
@Mock
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Mock
|
||||
private CrmDataVisibilityService crmDataVisibilityService;
|
||||
|
||||
@InjectMocks
|
||||
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
|
||||
void createSalesExpansion_shouldRejectDuplicateEmployeeNo() {
|
||||
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.UpdateOpportunityIntegrationRequest;
|
||||
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.unisbase.dto.DataScopeRuleDTO;
|
||||
import com.unisbase.security.PermissionService;
|
||||
import com.unisbase.service.DataScopeService;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
|
@ -50,7 +50,7 @@ class OpportunityServiceImplTest {
|
|||
private ObjectMapper objectMapper;
|
||||
|
||||
@Mock
|
||||
private DataScopeService dataScopeService;
|
||||
private CrmDataVisibilityService crmDataVisibilityService;
|
||||
|
||||
@Mock
|
||||
private UnisBaseTenantProvider tenantProvider;
|
||||
|
|
@ -217,10 +217,9 @@ class OpportunityServiceImplTest {
|
|||
|
||||
@Test
|
||||
void getOverview_shouldKeepConfiguredDataScopeAndIncludePreSalesVisibility() {
|
||||
DataScopeRuleDTO rule = new DataScopeRuleDTO();
|
||||
rule.setCreatorUserIds(List.of(2L, 3L, 2L));
|
||||
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();
|
||||
currentUser.setUserId(9L);
|
||||
currentUser.setUsername("presales.zhang");
|
||||
|
|
|
|||
|
|
@ -23,15 +23,21 @@ import com.unis.crm.mapper.OpportunityMapper;
|
|||
import java.time.LocalDate;
|
||||
import com.unis.crm.mapper.ProfileMapper;
|
||||
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.unisbase.service.SysPermissionService;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import java.util.List;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
|
@ -56,6 +62,15 @@ class WorkServiceImplTest {
|
|||
@Mock
|
||||
private UnisBaseTenantProvider tenantProvider;
|
||||
|
||||
@Mock
|
||||
private CrmDataVisibilityService crmDataVisibilityService;
|
||||
|
||||
@Mock
|
||||
private SysPermissionService sysPermissionService;
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private WorkServiceImpl workService;
|
||||
|
||||
private WorkReportProperties workReportProperties;
|
||||
|
|
@ -63,7 +78,19 @@ class WorkServiceImplTest {
|
|||
@BeforeEach
|
||||
void setUp() {
|
||||
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
|
||||
|
|
@ -86,6 +113,23 @@ class WorkServiceImplTest {
|
|||
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
|
||||
void getHistory_shouldOnlyQueryCheckInHistoryWhenTypeIsCheckin() {
|
||||
when(workMapper.selectCheckInHistory(2)).thenReturn(List.of(
|
||||
|
|
@ -167,6 +211,31 @@ class WorkServiceImplTest {
|
|||
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
|
||||
void saveDailyReport_shouldRejectEmptyContentByDefault() {
|
||||
when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
|
||||
|
|
@ -248,7 +317,7 @@ class WorkServiceImplTest {
|
|||
requestCaptor.getValue().getWorkContent().startsWith("1. 2026-04-28 整理客户资料,完成重点项目复盘"));
|
||||
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()).updateOpportunitySnapshot(anyLong(), any(), any(), any());
|
||||
verify(workMapper, never()).updateOpportunitySnapshot(anyLong(), any(), any(), any(), anyLong());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@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
|
||||
void exportDailyReports_shouldCleanMetadataAndFilterByBizType() {
|
||||
WorkDailyReportExportDTO opportunityReport = new WorkDailyReportExportDTO();
|
||||
|
|
@ -364,6 +474,47 @@ class WorkServiceImplTest {
|
|||
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
|
||||
void saveDailyReport_shouldNotUseTomorrowPlanAsOpportunityNextAction() {
|
||||
CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
|
||||
|
|
@ -542,6 +693,7 @@ class WorkServiceImplTest {
|
|||
when(opportunityMapper.selectDictLabel("sj_xmjd", "方案交流")).thenReturn(null);
|
||||
when(opportunityMapper.selectDictValueByLabel("sj_xmjd", "方案交流")).thenReturn("solution_discussion");
|
||||
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()))
|
||||
.thenReturn(1);
|
||||
when(workMapper.selectTodayReportId(eq(17L), any(LocalDate.class)))
|
||||
|
|
@ -555,7 +707,47 @@ class WorkServiceImplTest {
|
|||
eq(101L),
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -459,6 +459,7 @@ export interface OpportunityFollowUp {
|
|||
communicationContent?: string;
|
||||
nextAction?: string;
|
||||
user?: string;
|
||||
roleCodes?: string;
|
||||
attachments?: WorkReportAttachment[];
|
||||
}
|
||||
|
||||
|
|
@ -816,12 +817,14 @@ interface ApiErrorBody {
|
|||
|
||||
const LOGIN_PATH = "/login";
|
||||
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 PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
|
||||
const MY_PERMISSIONS_CACHE_KEY = "auth-cache:my-permissions";
|
||||
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
||||
const inFlightRequestCache = new Map<string, Promise<unknown>>();
|
||||
let authExpiryTimer: number | null = null;
|
||||
let refreshTokenPromise: Promise<string | null> | null = null;
|
||||
const AUTH_REQUIRED_MESSAGE_PATTERNS = [
|
||||
"登录已失效",
|
||||
"重新登录",
|
||||
|
|
@ -894,6 +897,15 @@ function hasAccessTokenExpired(token?: string | null, now = Date.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() {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
if (currentUrl.pathname === LOGIN_PATH && currentUrl.searchParams.get("timeout") === "1") {
|
||||
|
|
@ -927,6 +939,70 @@ function handleUnauthorizedResponse() {
|
|||
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() {
|
||||
clearAuthExpiryTimer();
|
||||
|
||||
|
|
@ -937,13 +1013,14 @@ function syncAuthExpiryTimer() {
|
|||
|
||||
const expiresAt = getAccessTokenExpiresAt(token);
|
||||
if (expiresAt === null || expiresAt <= Date.now()) {
|
||||
handleUnauthorizedResponse();
|
||||
void ensureFreshAccessToken(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshInMs = Math.max(0, expiresAt - Date.now() - REFRESH_AHEAD_MS);
|
||||
authExpiryTimer = window.setTimeout(() => {
|
||||
handleUnauthorizedResponse();
|
||||
}, expiresAt - Date.now());
|
||||
void ensureFreshAccessToken(true);
|
||||
}, refreshInMs);
|
||||
}
|
||||
|
||||
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) {
|
||||
const token = await ensureFreshAccessToken();
|
||||
if (!token) {
|
||||
throw new Error("登录已失效,请重新登录");
|
||||
}
|
||||
applyAuthHeaders(headers);
|
||||
}
|
||||
|
||||
|
|
@ -1083,12 +1164,26 @@ async function request<T>(input: string, init?: RequestInit, withAuth = false):
|
|||
}
|
||||
|
||||
export async function fetchWithAuth(input: string, init?: RequestInit) {
|
||||
const token = await ensureFreshAccessToken();
|
||||
if (!token) {
|
||||
throw new Error("登录已失效,请重新登录");
|
||||
}
|
||||
const response = await fetch(input, {
|
||||
...init,
|
||||
headers: applyAuthHeaders(new Headers(init?.headers)),
|
||||
});
|
||||
|
||||
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();
|
||||
throw new Error("登录已失效,请重新登录");
|
||||
}
|
||||
|
|
@ -1098,7 +1193,7 @@ export async function fetchWithAuth(input: string, init?: RequestInit) {
|
|||
|
||||
export function isAuthed() {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
return Boolean(token) && !hasAccessTokenExpired(token);
|
||||
return (Boolean(token) && !hasAccessTokenExpired(token)) || Boolean(localStorage.getItem("refreshToken"));
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
|
|
@ -1114,23 +1209,8 @@ export function clearAuth() {
|
|||
|
||||
export function persistLogin(payload: TokenResponse, username: string) {
|
||||
clearCachedAuthContext();
|
||||
localStorage.setItem("accessToken", payload.accessToken);
|
||||
localStorage.setItem("refreshToken", payload.refreshToken);
|
||||
localStorage.setItem("username", username);
|
||||
|
||||
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();
|
||||
persistTokenPair(payload);
|
||||
}
|
||||
|
||||
function getStoredUserId() {
|
||||
|
|
|
|||
|
|
@ -99,8 +99,9 @@ type OpportunityExportFilters = {
|
|||
operatorName?: string;
|
||||
hasSalesExpansion?: string;
|
||||
hasChannelExpansion?: string;
|
||||
selectedFields?: OpportunityExportFieldKey[];
|
||||
selectedFields?: LegacyOpportunityExportFieldKey[];
|
||||
};
|
||||
type LegacyOpportunityExportFieldKey = OpportunityExportFieldKey | "followUps";
|
||||
type OpportunityExportFieldKey =
|
||||
| "code"
|
||||
| "name"
|
||||
|
|
@ -119,7 +120,11 @@ type OpportunityExportFieldKey =
|
|||
| "isPoc"
|
||||
| "latestProgress"
|
||||
| "nextPlan"
|
||||
| "followUps"
|
||||
| "salesFollowUps"
|
||||
| "preSalesFollowUps"
|
||||
| "afterSalesFollowUps"
|
||||
| "productFollowUps"
|
||||
| "businessFollowUps"
|
||||
| "salesExpansionName"
|
||||
| "salesExpansionIntent"
|
||||
| "salesExpansionActive"
|
||||
|
|
@ -209,7 +214,7 @@ function loadOpportunityExportPreferences(): OpportunityExportFilters {
|
|||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(rawValue);
|
||||
return typeof parsed === "object" && parsed ? parsed as OpportunityExportFilters : {};
|
||||
return typeof parsed === "object" && parsed ? normalizeOpportunityExportFilters(parsed as OpportunityExportFilters) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
|
@ -220,12 +225,22 @@ function persistOpportunityExportPreferences(filters: OpportunityExportFilters)
|
|||
return;
|
||||
}
|
||||
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 {
|
||||
// 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) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
|
|
@ -290,7 +305,11 @@ const opportunityExportColumns: OpportunityExportColumn[] = [
|
|||
{ key: "competitorName", label: "竞争对手", value: (item) => normalizeOpportunityExportText(item.competitorName) },
|
||||
{ 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: "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: "salesExpansionIntent", label: "销售拓展人员合作意向", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent) },
|
||||
{ key: "salesExpansionActive", label: "销售拓展人员是否在职", value: (item, relatedSales) => {
|
||||
|
|
@ -327,20 +346,23 @@ const defaultOpportunityExportFields: OpportunityExportFieldKey[] = [
|
|||
"competitorName",
|
||||
"latestProgress",
|
||||
"nextPlan",
|
||||
"followUps",
|
||||
"salesFollowUps",
|
||||
"isPoc",
|
||||
];
|
||||
|
||||
function resolveSelectedOpportunityFields(selectedFields: OpportunityExportFieldKey[] | undefined) {
|
||||
return selectedFields === undefined ? defaultOpportunityExportFields : selectedFields;
|
||||
function resolveSelectedOpportunityFields(selectedFields: LegacyOpportunityExportFieldKey[] | undefined) {
|
||||
return selectedFields === undefined ? defaultOpportunityExportFields : normalizeOpportunityExportFieldKeys(selectedFields);
|
||||
}
|
||||
|
||||
function buildOpportunityFollowUpExportText(
|
||||
item: OpportunityItem,
|
||||
_relatedSales: SalesExpansionItem | null,
|
||||
_relatedChannel: ChannelExpansionItem | null,
|
||||
) {
|
||||
function normalizeOpportunityExportFieldKeys(selectedFields: LegacyOpportunityExportFieldKey[]) {
|
||||
const validFieldKeys = new Set(opportunityExportColumns.map((column) => column.key));
|
||||
const migratedFields = selectedFields.map((field) => field === "followUps" ? "salesFollowUps" : field);
|
||||
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 ?? [])
|
||||
.filter((record) => opportunityFollowUpHasRole(record, roleCode))
|
||||
.map((record) => {
|
||||
const summary = getOpportunityFollowUpSummary(record);
|
||||
const lines = [
|
||||
|
|
@ -353,6 +375,14 @@ function buildOpportunityFollowUpExportText(
|
|||
.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) {
|
||||
return normalizeOpportunityExportText(value).toLowerCase();
|
||||
}
|
||||
|
|
@ -2481,10 +2511,14 @@ export default function Opportunities() {
|
|||
wrapText: rowNumber > 1 && columns[columnNumber - 1]?.kind === "followup",
|
||||
};
|
||||
});
|
||||
const followUpColumnIndex = columns.findIndex((column) => column.kind === "followup") + 1;
|
||||
if (rowNumber > 1 && followUpColumnIndex > 0) {
|
||||
const followUpText = normalizeOpportunityExportText(row.getCell(followUpColumnIndex).value as string | null | undefined);
|
||||
const lineCount = followUpText ? followUpText.split("\n").length : 1;
|
||||
const followUpColumnIndexes = columns
|
||||
.map((column, index) => column.kind === "followup" ? index + 1 : 0)
|
||||
.filter((columnIndex) => columnIndex > 0);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import http from "./http";
|
||||
import {
|
||||
BotCredential, DeviceInfo, RoleDataScope, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
||||
PermissionNode
|
||||
PermissionNode, UserDataScopeAssignment, UserDataScopeGrant, UserDataScopeUser
|
||||
} from "../types";
|
||||
|
||||
export async function pageParams(params: SysParamQuery) {
|
||||
|
|
@ -199,6 +199,31 @@ export async function unbindUserFromRole(roleId: number, userId: number) {
|
|||
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) {
|
||||
const resp = await http.get("/sys/api/logs", { params });
|
||||
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 ReportReminderSettings } from "./system/report-reminder-settings";
|
||||
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 UserRoleBinding } from "./bindings/user-role";
|
||||
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 Dictionaries = lazy(() => import("@/pages/system/dictionaries"));
|
||||
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 DashboardAnalyticsSettings = lazy(() => import("@/features/dashboard-analytics/pages/dashboard-analytics-settings"));
|
||||
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: "/dictionaries", label: "字典管理", element: <LazyPage><Dictionaries /></LazyPage>, perm: "menu:dict" },
|
||||
{ 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: "/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" },
|
||||
|
|
|
|||
|
|
@ -121,6 +121,41 @@ export interface SysOrg extends BaseEntity {
|
|||
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 {
|
||||
id: 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),
|
||||
latest_progress text,
|
||||
next_plan text,
|
||||
updated_by bigint,
|
||||
archived boolean not null default false,
|
||||
archived_at timestamptz,
|
||||
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', 'latest_progress', '项目最新进展'),
|
||||
('crm_opportunity', 'next_plan', '下一步销售计划'),
|
||||
('crm_opportunity', 'updated_by', '更新人ID'),
|
||||
('crm_opportunity', 'archived', '是否归档'),
|
||||
('crm_opportunity', 'archived_at', '归档时间'),
|
||||
('crm_opportunity', 'pushed_to_oms', '是否已推送OMS'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue