feat: 搭建后端多租户名片服务
- 初始化 Spring Boot 多模块工程与通用基础能力 - 增加租户、组织、用户、名片、文件、统计等业务模块 - 补充数据库迁移脚本与基础测试
This commit is contained in:
50
backend/easycard-module-card/pom.xml
Normal file
50
backend/easycard-module-card/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>easycard-module-card</artifactId>
|
||||
<name>easycard-module-card</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-org</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-user</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-stat</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-file</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.easycard.module.card.controller;
|
||||
|
||||
import com.easycard.common.api.ApiResponse;
|
||||
import com.easycard.common.web.ClientRequestUtils;
|
||||
import com.easycard.module.card.service.CardProfileService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/open")
|
||||
public class OpenMiniappController {
|
||||
|
||||
private final CardProfileService cardProfileService;
|
||||
|
||||
public OpenMiniappController(CardProfileService cardProfileService) {
|
||||
this.cardProfileService = cardProfileService;
|
||||
}
|
||||
|
||||
@GetMapping("/firm/profile")
|
||||
public ApiResponse<CardProfileService.OpenFirmView> getFirmProfile() {
|
||||
return ApiResponse.success(cardProfileService.getOpenFirmProfile());
|
||||
}
|
||||
|
||||
@GetMapping("/cards")
|
||||
public ApiResponse<List<CardProfileService.OpenCardListItem>> listCards(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String office,
|
||||
@RequestParam(required = false) String practiceArea
|
||||
) {
|
||||
return ApiResponse.success(cardProfileService.listOpenCards(keyword, office, practiceArea));
|
||||
}
|
||||
|
||||
@GetMapping("/cards/{cardId}")
|
||||
public ApiResponse<CardProfileService.OpenCardDetailView> getCardDetail(
|
||||
@PathVariable Long cardId,
|
||||
@RequestParam(defaultValue = "DIRECT") String sourceType,
|
||||
@RequestParam(required = false) Long shareFromCardId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return ApiResponse.success(cardProfileService.getOpenCardDetail(
|
||||
cardId,
|
||||
sourceType,
|
||||
shareFromCardId,
|
||||
ClientRequestUtils.getClientIp(request),
|
||||
request.getRequestURI()
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/cards/{cardId}/share")
|
||||
public ApiResponse<Void> share(@PathVariable Long cardId, @RequestBody(required = false) ShareRequest request) {
|
||||
String shareChannel = request == null || request.shareChannel() == null ? "WECHAT_FRIEND" : request.shareChannel();
|
||||
String sharePath = request == null ? null : request.sharePath();
|
||||
cardProfileService.recordShare(cardId, shareChannel, sharePath);
|
||||
return ApiResponse.success("记录成功", null);
|
||||
}
|
||||
}
|
||||
|
||||
record ShareRequest(String shareChannel, String sharePath) {
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.easycard.module.card.controller;
|
||||
|
||||
import com.easycard.common.api.ApiResponse;
|
||||
import com.easycard.module.card.service.CardProfileService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tenant")
|
||||
public class TenantCardController {
|
||||
|
||||
private final CardProfileService cardProfileService;
|
||||
|
||||
public TenantCardController(CardProfileService cardProfileService) {
|
||||
this.cardProfileService = cardProfileService;
|
||||
}
|
||||
|
||||
@GetMapping("/cards")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<List<CardProfileService.CardSummaryView>> listCards(@RequestParam(required = false) String keyword) {
|
||||
return ApiResponse.success(cardProfileService.listTenantCards(keyword));
|
||||
}
|
||||
|
||||
@PostMapping("/cards")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.CardDetailView> createCard(@Valid @RequestBody UpsertCardCommand request) {
|
||||
return ApiResponse.success(cardProfileService.createCard(request.toServiceRequest()));
|
||||
}
|
||||
|
||||
@GetMapping("/cards/{cardId}")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.CardDetailView> getCard(@PathVariable Long cardId) {
|
||||
return ApiResponse.success(cardProfileService.getCardDetail(cardId));
|
||||
}
|
||||
|
||||
@PutMapping("/cards/{cardId}")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.CardDetailView> updateCard(@PathVariable Long cardId, @Valid @RequestBody UpsertCardCommand request) {
|
||||
return ApiResponse.success(cardProfileService.saveCard(cardId, request.toServiceRequest()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/cards/{cardId}")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<Void> deleteCard(@PathVariable Long cardId) {
|
||||
cardProfileService.deleteCard(cardId);
|
||||
return ApiResponse.success("删除成功", null);
|
||||
}
|
||||
|
||||
@PutMapping("/cards/sort")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<Void> sortCards(@Valid @RequestBody SortCardsRequest request) {
|
||||
cardProfileService.sortCards(request.cardIds());
|
||||
return ApiResponse.success("排序已生效", null);
|
||||
}
|
||||
|
||||
@GetMapping("/cards/me")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.CardDetailView> getMyCard() {
|
||||
return ApiResponse.success(cardProfileService.getMyCard());
|
||||
}
|
||||
|
||||
@PutMapping("/cards/me")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.CardDetailView> saveMyCard(@Valid @RequestBody UpsertCardCommand request) {
|
||||
return ApiResponse.success(cardProfileService.saveMyCard(request.toServiceRequest()));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/overview")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<CardProfileService.DashboardStatsView> overview() {
|
||||
return ApiResponse.success(cardProfileService.getDashboardStats());
|
||||
}
|
||||
|
||||
@GetMapping("/stats/trend")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<List<CardProfileService.CardTrendView>> trend(@RequestParam(defaultValue = "7") int days) {
|
||||
return ApiResponse.success(cardProfileService.getTrend(days));
|
||||
}
|
||||
}
|
||||
|
||||
record UpsertCardCommand(
|
||||
Long userId,
|
||||
Long deptId,
|
||||
@NotBlank(message = "名片姓名不能为空") String cardName,
|
||||
String cardTitle,
|
||||
String mobile,
|
||||
String telephone,
|
||||
String email,
|
||||
String officeAddress,
|
||||
Long avatarAssetId,
|
||||
Long coverAssetId,
|
||||
Long wechatQrAssetId,
|
||||
String bio,
|
||||
String certificateNo,
|
||||
String educationInfo,
|
||||
String honorInfo,
|
||||
@NotNull(message = "是否公开不能为空") Integer isPublic,
|
||||
@NotNull(message = "是否推荐不能为空") Integer isRecommended,
|
||||
@NotBlank(message = "发布状态不能为空") String publishStatus,
|
||||
Integer displayOrder,
|
||||
List<String> specialties
|
||||
) {
|
||||
CardProfileService.UpsertCardRequest toServiceRequest() {
|
||||
return new CardProfileService.UpsertCardRequest(
|
||||
userId, deptId, cardName, cardTitle, mobile, telephone, email, officeAddress,
|
||||
avatarAssetId, coverAssetId, wechatQrAssetId, bio, certificateNo, educationInfo,
|
||||
honorInfo, isPublic, isRecommended, publishStatus, displayOrder == null ? 0 : displayOrder, specialties
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
record SortCardsRequest(
|
||||
@Size(min = 1, message = "排序名片不能为空") List<@NotNull(message = "名片ID不能为空") Long> cardIds
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.easycard.module.card.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("card_profile")
|
||||
public class CardProfileDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private Long deptId;
|
||||
private String cardName;
|
||||
private String cardTitle;
|
||||
private String mobile;
|
||||
private String telephone;
|
||||
private String email;
|
||||
private String officeAddress;
|
||||
private Long avatarAssetId;
|
||||
private Long coverAssetId;
|
||||
private Long wechatQrAssetId;
|
||||
private String bio;
|
||||
private String certificateNo;
|
||||
private String educationInfo;
|
||||
private String honorInfo;
|
||||
private Integer isPublic;
|
||||
private Integer isRecommended;
|
||||
private String publishStatus;
|
||||
private Integer displayOrder;
|
||||
private Long viewCount;
|
||||
private Long shareCount;
|
||||
private LocalDateTime lastPublishedAt;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.easycard.module.card.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("card_profile_specialty")
|
||||
public class CardProfileSpecialtyDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long cardId;
|
||||
private Long practiceAreaId;
|
||||
private String specialtyName;
|
||||
private Integer displayOrder;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.card.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.card.dal.entity.CardProfileDO;
|
||||
|
||||
public interface CardProfileMapper extends BaseMapper<CardProfileDO> {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.easycard.module.card.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.card.dal.entity.CardProfileSpecialtyDO;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface CardProfileSpecialtyMapper extends BaseMapper<CardProfileSpecialtyDO> {
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM card_profile_specialty
|
||||
WHERE tenant_id = #{tenantId}
|
||||
AND card_id = #{cardId}
|
||||
""")
|
||||
int deleteForceByTenantIdAndCardId(@Param("tenantId") Long tenantId, @Param("cardId") Long cardId);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM card_profile_specialty
|
||||
WHERE tenant_id = #{tenantId}
|
||||
""")
|
||||
int deleteForceByTenantId(@Param("tenantId") Long tenantId);
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
package com.easycard.module.card.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.easycard.common.auth.LoginUser;
|
||||
import com.easycard.common.auth.SecurityUtils;
|
||||
import com.easycard.common.exception.BusinessException;
|
||||
import com.easycard.common.storage.StorageUrlUtils;
|
||||
import com.easycard.common.tenant.TenantContext;
|
||||
import com.easycard.common.tenant.TenantContextHolder;
|
||||
import com.easycard.module.card.dal.entity.CardProfileDO;
|
||||
import com.easycard.module.card.dal.entity.CardProfileSpecialtyDO;
|
||||
import com.easycard.module.card.dal.mapper.CardProfileMapper;
|
||||
import com.easycard.module.card.dal.mapper.CardProfileSpecialtyMapper;
|
||||
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||
import com.easycard.module.file.dal.mapper.FileAssetMapper;
|
||||
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||
import com.easycard.module.org.dal.entity.OrgFirmProfileDO;
|
||||
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||
import com.easycard.module.org.dal.mapper.OrgFirmProfileMapper;
|
||||
import com.easycard.module.stat.dal.entity.CardStatDailyDO;
|
||||
import com.easycard.module.stat.dal.mapper.CardStatDailyMapper;
|
||||
import com.easycard.module.stat.service.CardEventService;
|
||||
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||
import com.easycard.module.user.service.UserAuditService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CardProfileService {
|
||||
|
||||
private static final String AUTO_MANAGED_LAWYER_REMARK = "AUTO_MANAGED_LAWYER";
|
||||
private static final String AUTO_MANAGED_ROLE_CODE = "TENANT_USER";
|
||||
|
||||
@Value("${easycard.storage.public-endpoint:${easycard.storage.endpoint}}")
|
||||
private String publicEndpoint;
|
||||
|
||||
private final CardProfileMapper cardProfileMapper;
|
||||
private final CardProfileSpecialtyMapper cardProfileSpecialtyMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||
private final OrgFirmProfileMapper orgFirmProfileMapper;
|
||||
private final FileAssetMapper fileAssetMapper;
|
||||
private final CardStatDailyMapper cardStatDailyMapper;
|
||||
private final CardEventService cardEventService;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserAuditService userAuditService;
|
||||
|
||||
public List<CardSummaryView> listTenantCards(String keyword) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
List<CardProfileDO> cards = cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.like(StringUtils.hasText(keyword), CardProfileDO::getCardName, keyword)
|
||||
.orderByAsc(CardProfileDO::getDisplayOrder, CardProfileDO::getId));
|
||||
return filterLawyerCards(loginUser.tenantId(), cards).stream().map(this::toSummaryView).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CardDetailView createCard(UpsertCardRequest request) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||
SysUserDO hiddenUser = createHiddenLawyerUser(loginUser, request);
|
||||
|
||||
CardProfileDO card = new CardProfileDO();
|
||||
card.setTenantId(loginUser.tenantId());
|
||||
card.setUserId(hiddenUser.getId());
|
||||
card.setCreatedBy(loginUser.userId());
|
||||
card.setDeleted(0);
|
||||
card.setViewCount(0L);
|
||||
card.setShareCount(0L);
|
||||
applyUpsertRequest(card, request, loginUser.userId(), true);
|
||||
card.setDisplayOrder(nextCardDisplayOrder(loginUser.tenantId()));
|
||||
cardProfileMapper.insert(card);
|
||||
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||
return toDetailView(card);
|
||||
}
|
||||
|
||||
public CardDetailView getCardDetail(Long cardId) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||
return toDetailView(card);
|
||||
}
|
||||
|
||||
public CardDetailView getMyCard() {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardProfileDO::getUserId, loginUser.userId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (card == null) {
|
||||
SysUserDO user = sysUserMapper.selectById(loginUser.userId());
|
||||
card = new CardProfileDO();
|
||||
card.setTenantId(loginUser.tenantId());
|
||||
card.setUserId(loginUser.userId());
|
||||
card.setDeptId(user == null ? null : user.getDeptId());
|
||||
card.setCardName(user == null ? loginUser.realName() : user.getRealName());
|
||||
card.setCardTitle(user == null ? "" : user.getJobTitle());
|
||||
card.setMobile(user == null ? "" : user.getMobile());
|
||||
card.setEmail(user == null ? "" : user.getEmail());
|
||||
card.setOfficeAddress("");
|
||||
card.setIsPublic(1);
|
||||
card.setIsRecommended(0);
|
||||
card.setPublishStatus("DRAFT");
|
||||
card.setDisplayOrder(999);
|
||||
card.setViewCount(0L);
|
||||
card.setShareCount(0L);
|
||||
card.setCreatedBy(loginUser.userId());
|
||||
card.setUpdatedBy(loginUser.userId());
|
||||
card.setDeleted(0);
|
||||
cardProfileMapper.insert(card);
|
||||
}
|
||||
return toDetailView(card);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CardDetailView saveMyCard(UpsertCardRequest request) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardProfileDO::getUserId, loginUser.userId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (card == null) {
|
||||
card = new CardProfileDO();
|
||||
card.setTenantId(loginUser.tenantId());
|
||||
card.setUserId(loginUser.userId());
|
||||
card.setCreatedBy(loginUser.userId());
|
||||
card.setDeleted(0);
|
||||
card.setViewCount(0L);
|
||||
card.setShareCount(0L);
|
||||
}
|
||||
applyUpsertRequest(card, request, loginUser.userId(), false);
|
||||
if (card.getId() == null) {
|
||||
cardProfileMapper.insert(card);
|
||||
} else {
|
||||
cardProfileMapper.updateById(card);
|
||||
}
|
||||
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||
return toDetailView(card);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CardDetailView saveCard(Long cardId, UpsertCardRequest request) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||
applyUpsertRequest(card, request, loginUser.userId(), true);
|
||||
cardProfileMapper.updateById(card);
|
||||
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||
return toDetailView(card);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteCard(Long cardId) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||
Long userId = card.getUserId();
|
||||
|
||||
card.setDeleted(1);
|
||||
card.setUpdatedBy(loginUser.userId());
|
||||
cardProfileMapper.updateById(card);
|
||||
cardProfileSpecialtyMapper.deleteForceByTenantIdAndCardId(loginUser.tenantId(), cardId);
|
||||
|
||||
if (userId != null) {
|
||||
SysUserDO user = sysUserMapper.selectById(userId);
|
||||
if (user != null
|
||||
&& loginUser.tenantId().equals(user.getTenantId())
|
||||
&& Integer.valueOf(0).equals(user.getDeleted())
|
||||
&& AUTO_MANAGED_LAWYER_REMARK.equals(user.getRemark())) {
|
||||
sysUserRoleMapper.delete(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||
.eq(SysUserRoleDO::getTenantId, loginUser.tenantId())
|
||||
.eq(SysUserRoleDO::getUserId, userId));
|
||||
user.setDeleted(1);
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
sysUserMapper.updateById(user);
|
||||
}
|
||||
}
|
||||
|
||||
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "CARD", "CARD_PROFILE", cardId, "DELETE");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void sortCards(List<Long> cardIds) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
if (cardIds == null || cardIds.isEmpty()) {
|
||||
throw new BusinessException("CARD_SORT_INVALID", "排序数据不能为空");
|
||||
}
|
||||
Set<Long> uniqueCardIds = new HashSet<>(cardIds);
|
||||
if (uniqueCardIds.size() != cardIds.size()) {
|
||||
throw new BusinessException("CARD_SORT_INVALID", "排序数据存在重复名片");
|
||||
}
|
||||
|
||||
List<CardProfileDO> tenantCards = filterLawyerCards(loginUser.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.select(CardProfileDO::getId, CardProfileDO::getUserId)));
|
||||
Set<Long> expectedCardIds = tenantCards.stream().map(CardProfileDO::getId).collect(Collectors.toSet());
|
||||
if (expectedCardIds.size() != uniqueCardIds.size() || !expectedCardIds.equals(uniqueCardIds)) {
|
||||
throw new BusinessException("CARD_SORT_INVALID", "排序数据必须包含当前租户全部律师名片");
|
||||
}
|
||||
|
||||
Map<Long, CardProfileDO> cardMap = cardProfileMapper.selectBatchIds(cardIds).stream()
|
||||
.filter(item -> loginUser.tenantId().equals(item.getTenantId()) && Integer.valueOf(0).equals(item.getDeleted()))
|
||||
.collect(Collectors.toMap(CardProfileDO::getId, Function.identity()));
|
||||
for (int i = 0; i < cardIds.size(); i++) {
|
||||
Long cardId = cardIds.get(i);
|
||||
CardProfileDO card = cardMap.get(cardId);
|
||||
if (card == null) {
|
||||
throw new BusinessException("CARD_SORT_INVALID", "律师名片不存在或不属于当前租户");
|
||||
}
|
||||
card.setDisplayOrder((i + 1) * 10);
|
||||
card.setUpdatedBy(loginUser.userId());
|
||||
cardProfileMapper.updateById(card);
|
||||
}
|
||||
|
||||
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "CARD", "CARD_PROFILE", 0L, "SORT");
|
||||
}
|
||||
|
||||
public DashboardStatsView getDashboardStats() {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
List<CardProfileDO> cards = filterLawyerCards(loginUser.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardProfileDO::getDeleted, 0)));
|
||||
long publishedCount = cards.stream().filter(item -> "PUBLISHED".equals(item.getPublishStatus()) && Integer.valueOf(1).equals(item.getIsPublic())).count();
|
||||
long totalViews = cards.stream().mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount()).sum();
|
||||
long totalShares = cards.stream().mapToLong(item -> item.getShareCount() == null ? 0L : item.getShareCount()).sum();
|
||||
Set<Long> lawyerCardIds = cards.stream().map(CardProfileDO::getId).collect(Collectors.toSet());
|
||||
long todayViews = cardStatDailyMapper.selectList(Wrappers.<CardStatDailyDO>lambdaQuery()
|
||||
.eq(CardStatDailyDO::getTenantId, loginUser.tenantId())
|
||||
.eq(CardStatDailyDO::getStatDate, LocalDate.now()))
|
||||
.stream()
|
||||
.filter(item -> lawyerCardIds.contains(item.getCardId()))
|
||||
.mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount())
|
||||
.sum();
|
||||
return new DashboardStatsView(cards.size(), publishedCount, totalViews, totalShares, todayViews);
|
||||
}
|
||||
|
||||
public List<CardTrendView> getTrend(int days) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
LocalDate start = LocalDate.now().minusDays(Math.max(days - 1, 0));
|
||||
return cardStatDailyMapper.selectList(Wrappers.<CardStatDailyDO>lambdaQuery()
|
||||
.eq(CardStatDailyDO::getTenantId, loginUser.tenantId())
|
||||
.ge(CardStatDailyDO::getStatDate, start)
|
||||
.orderByAsc(CardStatDailyDO::getStatDate))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(CardStatDailyDO::getStatDate))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> new CardTrendView(
|
||||
entry.getKey().toString(),
|
||||
entry.getValue().stream().mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount()).sum(),
|
||||
entry.getValue().stream().mapToLong(item -> item.getShareCount() == null ? 0L : item.getShareCount()).sum()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public OpenFirmView getOpenFirmProfile() {
|
||||
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||
.eq(OrgFirmProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (profile == null) {
|
||||
throw new BusinessException("FIRM_PROFILE_NOT_FOUND", "事务所主页未配置");
|
||||
}
|
||||
List<String> offices = orgDepartmentMapper.selectList(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||
.eq(OrgDepartmentDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(OrgDepartmentDO::getDeleted, 0)
|
||||
.eq(OrgDepartmentDO::getDeptStatus, "ENABLED")
|
||||
.orderByAsc(OrgDepartmentDO::getDisplayOrder, OrgDepartmentDO::getId))
|
||||
.stream()
|
||||
.map(OrgDepartmentDO::getDeptName)
|
||||
.toList();
|
||||
List<String> areas = cardProfileSpecialtyMapper.selectList(Wrappers.<CardProfileSpecialtyDO>lambdaQuery()
|
||||
.eq(CardProfileSpecialtyDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(CardProfileSpecialtyDO::getDeleted, 0))
|
||||
.stream()
|
||||
.map(CardProfileSpecialtyDO::getSpecialtyName)
|
||||
.distinct()
|
||||
.toList();
|
||||
long lawyerCount = filterLawyerCards(tenantContext.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||
.eq(CardProfileDO::getIsPublic, 1)))
|
||||
.size();
|
||||
return new OpenFirmView(
|
||||
profile.getFirmName(),
|
||||
resolveAssetUrl(profile.getLogoAssetId()),
|
||||
resolveAssetUrl(profile.getHeroAssetId()),
|
||||
profile.getIntro(),
|
||||
profile.getHotlinePhone(),
|
||||
profile.getHqAddress(),
|
||||
profile.getHqLatitude() == null ? null : profile.getHqLatitude().doubleValue(),
|
||||
profile.getHqLongitude() == null ? null : profile.getHqLongitude().doubleValue(),
|
||||
(long) offices.size(),
|
||||
lawyerCount,
|
||||
offices,
|
||||
areas
|
||||
);
|
||||
}
|
||||
|
||||
public List<OpenCardListItem> listOpenCards(String keyword, String office, String practiceArea) {
|
||||
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||
List<CardProfileDO> cards = filterLawyerCards(tenantContext.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||
.eq(CardProfileDO::getIsPublic, 1)
|
||||
.orderByAsc(CardProfileDO::getDisplayOrder, CardProfileDO::getId)));
|
||||
if (cards.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> deptIds = cards.stream().map(CardProfileDO::getDeptId).filter(id -> id != null && id > 0).distinct().toList();
|
||||
Map<Long, OrgDepartmentDO> deptMap = deptIds.isEmpty() ? Map.of() : orgDepartmentMapper.selectBatchIds(deptIds)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(OrgDepartmentDO::getId, Function.identity()));
|
||||
Map<Long, List<String>> specialtyMap = loadSpecialtyMap(cards.stream().map(CardProfileDO::getId).toList());
|
||||
return cards.stream()
|
||||
.filter(card -> {
|
||||
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||
List<String> specialties = specialtyMap.getOrDefault(card.getId(), List.of());
|
||||
boolean keywordMatched = !StringUtils.hasText(keyword)
|
||||
|| card.getCardName().contains(keyword)
|
||||
|| deptName.contains(keyword)
|
||||
|| specialties.stream().anyMatch(item -> item.contains(keyword));
|
||||
boolean officeMatched = !StringUtils.hasText(office) || office.equals(deptName);
|
||||
boolean areaMatched = !StringUtils.hasText(practiceArea) || specialties.stream().anyMatch(item -> item.equals(practiceArea));
|
||||
return keywordMatched && officeMatched && areaMatched;
|
||||
})
|
||||
.map(card -> {
|
||||
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||
return new OpenCardListItem(
|
||||
card.getId(),
|
||||
card.getCardName(),
|
||||
card.getCardTitle(),
|
||||
deptName,
|
||||
card.getMobile(),
|
||||
card.getEmail(),
|
||||
resolveAssetUrl(card.getAvatarAssetId()),
|
||||
specialtyMap.getOrDefault(card.getId(), List.of())
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OpenCardDetailView getOpenCardDetail(Long cardId, String sourceType, Long shareFromCardId, String viewerIp, String pagePath) {
|
||||
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(CardProfileDO::getId, cardId)
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||
.eq(CardProfileDO::getIsPublic, 1)
|
||||
.last("LIMIT 1"));
|
||||
if (card == null || !hasLawyerRole(tenantContext.tenantId(), card.getUserId())) {
|
||||
throw new BusinessException("CARD_NOT_FOUND", "名片不存在或未公开");
|
||||
}
|
||||
card.setViewCount((card.getViewCount() == null ? 0L : card.getViewCount()) + 1);
|
||||
cardProfileMapper.updateById(card);
|
||||
cardEventService.recordView(tenantContext.tenantId(), tenantContext.miniappAppId(), card.getId(), sourceType, shareFromCardId, viewerIp, pagePath);
|
||||
OrgDepartmentDO department = card.getDeptId() == null ? null : orgDepartmentMapper.selectById(card.getDeptId());
|
||||
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||
.eq(OrgFirmProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
return new OpenCardDetailView(
|
||||
card.getId(),
|
||||
card.getCardName(),
|
||||
card.getCardTitle(),
|
||||
department == null ? "" : department.getDeptName(),
|
||||
card.getMobile(),
|
||||
card.getEmail(),
|
||||
card.getOfficeAddress(),
|
||||
resolveAssetUrl(card.getAvatarAssetId()),
|
||||
resolveAssetUrl(card.getCoverAssetId()),
|
||||
resolveAssetUrl(card.getWechatQrAssetId()),
|
||||
card.getBio(),
|
||||
loadSpecialtyMap(List.of(card.getId())).getOrDefault(card.getId(), List.of()),
|
||||
profile == null ? "" : profile.getFirmName(),
|
||||
profile == null ? "" : profile.getHqAddress(),
|
||||
profile == null || profile.getHqLatitude() == null ? null : profile.getHqLatitude().doubleValue(),
|
||||
profile == null || profile.getHqLongitude() == null ? null : profile.getHqLongitude().doubleValue()
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void recordShare(Long cardId, String shareChannel, String sharePath) {
|
||||
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||
.eq(CardProfileDO::getId, cardId)
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (card == null || !hasLawyerRole(tenantContext.tenantId(), card.getUserId())) {
|
||||
throw new BusinessException("CARD_NOT_FOUND", "名片不存在");
|
||||
}
|
||||
card.setShareCount((card.getShareCount() == null ? 0L : card.getShareCount()) + 1);
|
||||
cardProfileMapper.updateById(card);
|
||||
cardEventService.recordShare(tenantContext.tenantId(), tenantContext.miniappAppId(), cardId, shareChannel, sharePath);
|
||||
}
|
||||
|
||||
private CardProfileDO getRequiredTenantCard(Long cardId, Long tenantId) {
|
||||
CardProfileDO card = cardProfileMapper.selectById(cardId);
|
||||
if (card == null || Integer.valueOf(1).equals(card.getDeleted()) || !tenantId.equals(card.getTenantId())) {
|
||||
throw new BusinessException("CARD_NOT_FOUND", "名片不存在");
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private void validateDepartment(Long tenantId, Long deptId) {
|
||||
if (deptId == null) {
|
||||
return;
|
||||
}
|
||||
OrgDepartmentDO department = orgDepartmentMapper.selectById(deptId);
|
||||
if (department == null || Integer.valueOf(1).equals(department.getDeleted()) || !tenantId.equals(department.getTenantId())) {
|
||||
throw new BusinessException("DEPARTMENT_NOT_FOUND", "所属组织不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private List<CardProfileDO> filterLawyerCards(Long tenantId, List<CardProfileDO> cards) {
|
||||
if (cards == null || cards.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, String> roleCodeMap = loadUserRoleCodeMap(
|
||||
tenantId,
|
||||
cards.stream()
|
||||
.map(CardProfileDO::getUserId)
|
||||
.filter(userId -> userId != null && userId > 0)
|
||||
.distinct()
|
||||
.toList()
|
||||
);
|
||||
return cards.stream()
|
||||
.filter(card -> isLawyerRoleCode(roleCodeMap.get(card.getUserId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean hasLawyerRole(Long tenantId, Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
return false;
|
||||
}
|
||||
return isLawyerRoleCode(loadUserRoleCodeMap(tenantId, List.of(userId)).get(userId));
|
||||
}
|
||||
|
||||
private Map<Long, String> loadUserRoleCodeMap(Long tenantId, List<Long> userIds) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||
.eq(SysUserRoleDO::getTenantId, tenantId)
|
||||
.in(SysUserRoleDO::getUserId, userIds));
|
||||
if (userRoles.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).distinct().toList();
|
||||
Map<Long, String> roleIdCodeMap = sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||
.eq(SysRoleDO::getTenantId, tenantId)
|
||||
.eq(SysRoleDO::getDeleted, 0)
|
||||
.in(SysRoleDO::getId, roleIds))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(SysRoleDO::getId, SysRoleDO::getRoleCode));
|
||||
return userRoles.stream().collect(Collectors.toMap(
|
||||
SysUserRoleDO::getUserId,
|
||||
item -> roleIdCodeMap.getOrDefault(item.getRoleId(), ""),
|
||||
(left, right) -> left
|
||||
));
|
||||
}
|
||||
|
||||
private boolean isLawyerRoleCode(String roleCode) {
|
||||
return AUTO_MANAGED_ROLE_CODE.equals(roleCode);
|
||||
}
|
||||
|
||||
private SysUserDO createHiddenLawyerUser(LoginUser loginUser, UpsertCardRequest request) {
|
||||
SysRoleDO role = getRequiredTenantRole(loginUser.tenantId(), AUTO_MANAGED_ROLE_CODE);
|
||||
SysUserDO user = new SysUserDO();
|
||||
user.setTenantId(loginUser.tenantId());
|
||||
user.setUserType("TENANT");
|
||||
user.setUsername(generateHiddenLawyerUsername(loginUser.tenantId()));
|
||||
user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||
user.setRealName(request.cardName());
|
||||
user.setNickName(request.cardName());
|
||||
user.setGender("UNKNOWN");
|
||||
user.setMobile(request.mobile());
|
||||
user.setEmail(request.email());
|
||||
user.setDeptId(request.deptId());
|
||||
user.setJobTitle(request.cardTitle());
|
||||
user.setUserStatus("DISABLED");
|
||||
user.setMustUpdatePassword(0);
|
||||
user.setRemark(AUTO_MANAGED_LAWYER_REMARK);
|
||||
user.setCreatedBy(loginUser.userId());
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
user.setDeleted(0);
|
||||
sysUserMapper.insert(user);
|
||||
|
||||
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||
userRole.setTenantId(loginUser.tenantId());
|
||||
userRole.setUserId(user.getId());
|
||||
userRole.setRoleId(role.getId());
|
||||
userRole.setCreatedBy(loginUser.userId());
|
||||
sysUserRoleMapper.insert(userRole);
|
||||
return user;
|
||||
}
|
||||
|
||||
private String generateHiddenLawyerUsername(Long tenantId) {
|
||||
String base = "lawyer_" + tenantId + "_" + System.currentTimeMillis();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String candidate = i == 0 ? base : base + "_" + i;
|
||||
Long count = sysUserMapper.selectCount(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getTenantId, tenantId)
|
||||
.eq(SysUserDO::getUsername, candidate)
|
||||
.eq(SysUserDO::getDeleted, 0));
|
||||
if (count == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return base + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
}
|
||||
|
||||
private SysRoleDO getRequiredTenantRole(Long tenantId, String roleCode) {
|
||||
SysRoleDO role = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||
.eq(SysRoleDO::getTenantId, tenantId)
|
||||
.eq(SysRoleDO::getRoleCode, roleCode)
|
||||
.eq(SysRoleDO::getDeleted, 0)
|
||||
.eq(SysRoleDO::getRoleStatus, "ENABLED")
|
||||
.last("LIMIT 1"));
|
||||
if (role == null) {
|
||||
throw new BusinessException("ROLE_NOT_FOUND", "角色不存在");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private Integer nextCardDisplayOrder(Long tenantId) {
|
||||
CardProfileDO lastCard = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||
.eq(CardProfileDO::getTenantId, tenantId)
|
||||
.eq(CardProfileDO::getDeleted, 0)
|
||||
.orderByDesc(CardProfileDO::getDisplayOrder)
|
||||
.orderByDesc(CardProfileDO::getId)
|
||||
.last("LIMIT 1"));
|
||||
int baseSort = lastCard == null || lastCard.getDisplayOrder() == null ? 0 : lastCard.getDisplayOrder();
|
||||
return baseSort + 10;
|
||||
}
|
||||
|
||||
private void applyUpsertRequest(CardProfileDO card, UpsertCardRequest request, Long operatorId, boolean allowChangeUser) {
|
||||
if (allowChangeUser && request.userId() != null) {
|
||||
card.setUserId(request.userId());
|
||||
}
|
||||
card.setDeptId(request.deptId());
|
||||
card.setCardName(request.cardName());
|
||||
card.setCardTitle(request.cardTitle());
|
||||
card.setMobile(request.mobile());
|
||||
card.setTelephone(request.telephone());
|
||||
card.setEmail(request.email());
|
||||
card.setOfficeAddress(request.officeAddress());
|
||||
card.setAvatarAssetId(request.avatarAssetId());
|
||||
card.setCoverAssetId(request.coverAssetId());
|
||||
card.setWechatQrAssetId(request.wechatQrAssetId());
|
||||
card.setBio(request.bio());
|
||||
card.setCertificateNo(request.certificateNo());
|
||||
card.setEducationInfo(request.educationInfo());
|
||||
card.setHonorInfo(request.honorInfo());
|
||||
card.setIsPublic(request.isPublic());
|
||||
card.setIsRecommended(request.isRecommended());
|
||||
card.setPublishStatus(request.publishStatus());
|
||||
card.setDisplayOrder(request.displayOrder());
|
||||
if ("PUBLISHED".equals(request.publishStatus())) {
|
||||
card.setLastPublishedAt(LocalDateTime.now());
|
||||
}
|
||||
card.setUpdatedBy(operatorId);
|
||||
}
|
||||
|
||||
private void replaceSpecialties(Long tenantId, Long cardId, List<String> specialties, Long operatorId) {
|
||||
cardProfileSpecialtyMapper.deleteForceByTenantIdAndCardId(tenantId, cardId);
|
||||
List<String> normalizedSpecialties = normalizeSpecialties(specialties);
|
||||
if (normalizedSpecialties.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < normalizedSpecialties.size(); i++) {
|
||||
String specialty = normalizedSpecialties.get(i);
|
||||
CardProfileSpecialtyDO specialtyDO = new CardProfileSpecialtyDO();
|
||||
specialtyDO.setTenantId(tenantId);
|
||||
specialtyDO.setCardId(cardId);
|
||||
specialtyDO.setSpecialtyName(specialty);
|
||||
specialtyDO.setDisplayOrder((i + 1) * 10);
|
||||
specialtyDO.setCreatedBy(operatorId);
|
||||
specialtyDO.setUpdatedBy(operatorId);
|
||||
specialtyDO.setDeleted(0);
|
||||
cardProfileSpecialtyMapper.insert(specialtyDO);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> normalizeSpecialties(List<String> specialties) {
|
||||
if (specialties == null || specialties.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Set<String> normalized = new LinkedHashSet<>();
|
||||
for (String specialty : specialties) {
|
||||
if (!StringUtils.hasText(specialty)) {
|
||||
continue;
|
||||
}
|
||||
normalized.add(specialty.trim());
|
||||
}
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
private CardSummaryView toSummaryView(CardProfileDO card) {
|
||||
return new CardSummaryView(
|
||||
card.getId(),
|
||||
card.getUserId(),
|
||||
card.getCardName(),
|
||||
card.getCardTitle(),
|
||||
card.getMobile(),
|
||||
card.getEmail(),
|
||||
card.getPublishStatus(),
|
||||
card.getDisplayOrder(),
|
||||
card.getCreatedTime(),
|
||||
card.getUpdatedTime()
|
||||
);
|
||||
}
|
||||
|
||||
private CardDetailView toDetailView(CardProfileDO card) {
|
||||
SysUserDO user = sysUserMapper.selectById(card.getUserId());
|
||||
OrgDepartmentDO department = card.getDeptId() == null ? null : orgDepartmentMapper.selectById(card.getDeptId());
|
||||
return new CardDetailView(
|
||||
card.getId(),
|
||||
card.getUserId(),
|
||||
user == null ? "" : user.getUsername(),
|
||||
card.getDeptId(),
|
||||
department == null ? "" : department.getDeptName(),
|
||||
card.getCardName(),
|
||||
card.getCardTitle(),
|
||||
card.getMobile(),
|
||||
card.getTelephone(),
|
||||
card.getEmail(),
|
||||
card.getOfficeAddress(),
|
||||
card.getAvatarAssetId(),
|
||||
card.getCoverAssetId(),
|
||||
card.getWechatQrAssetId(),
|
||||
resolveAssetUrl(card.getAvatarAssetId()),
|
||||
resolveAssetUrl(card.getCoverAssetId()),
|
||||
resolveAssetUrl(card.getWechatQrAssetId()),
|
||||
card.getBio(),
|
||||
card.getCertificateNo(),
|
||||
card.getEducationInfo(),
|
||||
card.getHonorInfo(),
|
||||
card.getIsPublic(),
|
||||
card.getIsRecommended(),
|
||||
card.getPublishStatus(),
|
||||
card.getDisplayOrder(),
|
||||
loadSpecialtyMap(List.of(card.getId())).getOrDefault(card.getId(), List.of())
|
||||
);
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> loadSpecialtyMap(List<Long> cardIds) {
|
||||
if (cardIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return cardProfileSpecialtyMapper.selectList(Wrappers.<CardProfileSpecialtyDO>lambdaQuery()
|
||||
.in(CardProfileSpecialtyDO::getCardId, cardIds)
|
||||
.eq(CardProfileSpecialtyDO::getDeleted, 0)
|
||||
.orderByAsc(CardProfileSpecialtyDO::getDisplayOrder, CardProfileSpecialtyDO::getId))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
CardProfileSpecialtyDO::getCardId,
|
||||
Collectors.mapping(CardProfileSpecialtyDO::getSpecialtyName, Collectors.toList())
|
||||
));
|
||||
}
|
||||
|
||||
private String resolveAssetUrl(Long assetId) {
|
||||
if (assetId == null) {
|
||||
return "";
|
||||
}
|
||||
FileAssetDO asset = fileAssetMapper.selectById(assetId);
|
||||
return asset == null ? "" : StorageUrlUtils.buildPublicUrl(publicEndpoint, asset.getBucketName(), asset.getObjectKey(), asset.getAccessUrl());
|
||||
}
|
||||
|
||||
public record UpsertCardRequest(
|
||||
Long userId,
|
||||
Long deptId,
|
||||
String cardName,
|
||||
String cardTitle,
|
||||
String mobile,
|
||||
String telephone,
|
||||
String email,
|
||||
String officeAddress,
|
||||
Long avatarAssetId,
|
||||
Long coverAssetId,
|
||||
Long wechatQrAssetId,
|
||||
String bio,
|
||||
String certificateNo,
|
||||
String educationInfo,
|
||||
String honorInfo,
|
||||
Integer isPublic,
|
||||
Integer isRecommended,
|
||||
String publishStatus,
|
||||
Integer displayOrder,
|
||||
List<String> specialties
|
||||
) {
|
||||
}
|
||||
|
||||
public record CardSummaryView(
|
||||
Long id,
|
||||
Long userId,
|
||||
String cardName,
|
||||
String cardTitle,
|
||||
String mobile,
|
||||
String email,
|
||||
String publishStatus,
|
||||
Integer displayOrder,
|
||||
LocalDateTime createdTime,
|
||||
LocalDateTime updatedTime
|
||||
) {
|
||||
}
|
||||
|
||||
public record CardDetailView(
|
||||
Long id,
|
||||
Long userId,
|
||||
String username,
|
||||
Long deptId,
|
||||
String deptName,
|
||||
String cardName,
|
||||
String cardTitle,
|
||||
String mobile,
|
||||
String telephone,
|
||||
String email,
|
||||
String officeAddress,
|
||||
Long avatarAssetId,
|
||||
Long coverAssetId,
|
||||
Long wechatQrAssetId,
|
||||
String avatarUrl,
|
||||
String coverUrl,
|
||||
String wechatQrUrl,
|
||||
String bio,
|
||||
String certificateNo,
|
||||
String educationInfo,
|
||||
String honorInfo,
|
||||
Integer isPublic,
|
||||
Integer isRecommended,
|
||||
String publishStatus,
|
||||
Integer displayOrder,
|
||||
List<String> specialties
|
||||
) {
|
||||
}
|
||||
|
||||
public record DashboardStatsView(Integer totalCards, Long publishedCards, Long totalViews, Long totalShares, Long todayViews) {
|
||||
}
|
||||
|
||||
public record CardTrendView(String statDate, Long viewCount, Long shareCount) {
|
||||
}
|
||||
|
||||
public record OpenFirmView(
|
||||
String name,
|
||||
String logo,
|
||||
String heroImage,
|
||||
String intro,
|
||||
String hotlinePhone,
|
||||
String hqAddress,
|
||||
Double hqLatitude,
|
||||
Double hqLongitude,
|
||||
Long officeCount,
|
||||
Long lawyerCount,
|
||||
List<String> officeList,
|
||||
List<String> practiceAreas
|
||||
) {
|
||||
}
|
||||
|
||||
public record OpenCardListItem(
|
||||
Long id,
|
||||
String name,
|
||||
String title,
|
||||
String office,
|
||||
String phone,
|
||||
String email,
|
||||
String avatar,
|
||||
List<String> specialties
|
||||
) {
|
||||
}
|
||||
|
||||
public record OpenCardDetailView(
|
||||
Long id,
|
||||
String name,
|
||||
String title,
|
||||
String office,
|
||||
String phone,
|
||||
String email,
|
||||
String address,
|
||||
String avatar,
|
||||
String coverImage,
|
||||
String wechatQrImage,
|
||||
String bio,
|
||||
List<String> specialties,
|
||||
String firmName,
|
||||
String firmAddress,
|
||||
Double firmLatitude,
|
||||
Double firmLongitude
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user