Sfoglia il codice sorgente

账号订阅/取消订阅服务能力抽象

wangqi49 3 mesi fa
parent
commit
eb2ceecf5f
26 ha cambiato i file con 805 aggiunte e 10 eliminazioni
  1. 14 0
      resources/database-sql/webchat-user.sql
  2. 28 0
      webchat-admin/src/main/java/com/webchat/admin/controller/AccountManagementController.java
  3. 27 5
      webchat-admin/src/main/java/com/webchat/admin/service/AccountManagementService.java
  4. 15 4
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/AccountController.java
  5. 44 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/AccountRelationController.java
  6. 40 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/service/AccountRelationService.java
  7. 16 1
      webchat-client-chat/src/main/java/com/webchat/client/chat/service/AccountService.java
  8. 20 0
      webchat-common/src/main/java/com/webchat/common/enums/AccountRelationTypeEnum.java
  9. 5 0
      webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java
  10. 7 0
      webchat-common/src/main/java/com/webchat/common/enums/RoleCodeEnum.java
  11. 34 0
      webchat-remote/src/main/java/com/webchat/rmi/user/AccountRelationClient.java
  12. 10 0
      webchat-remote/src/main/java/com/webchat/rmi/user/UserServiceClient.java
  13. 24 0
      webchat-user/src/main/java/com/webchat/user/controller/AccountRelationController.java
  14. 6 0
      webchat-user/src/main/java/com/webchat/user/controller/UserServiceController.java
  15. 16 0
      webchat-user/src/main/java/com/webchat/user/repository/dao/IAccountRelationDAO.java
  16. 43 0
      webchat-user/src/main/java/com/webchat/user/repository/entity/AccountRelationEntity.java
  17. 41 0
      webchat-user/src/main/java/com/webchat/user/service/UserService.java
  18. 176 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AbstractAccountRelationService.java
  19. 43 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationFactory.java
  20. 42 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationService.java
  21. 15 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationValidator.java
  22. 24 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationWapper.java
  23. 27 0
      webchat-user/src/main/java/com/webchat/user/service/relation/User2GroupAccountRelationService.java
  24. 27 0
      webchat-user/src/main/java/com/webchat/user/service/relation/User2OfficialAccountRelationService.java
  25. 26 0
      webchat-user/src/main/java/com/webchat/user/service/relation/User2RobotAccountRelationService.java
  26. 35 0
      webchat-user/src/main/java/com/webchat/user/service/relation/User2UserAccountRelationService.java

+ 14 - 0
resources/database-sql/webchat-user.sql

@@ -59,3 +59,17 @@ CREATE TABLE webchat_user.`web_chat_group_user` (
                                        KEY `INDEX_USER_ID` (`USER_ID`),
                                        KEY `INDEX_STATUS` (`STATUS`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='群组用户表';
+
+
+-- 账号关系表
+CREATE TABLE webchat_user.`web_chat_account_relation` (
+              `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关系主键',
+              `source_account` char(32) NOT NULL COMMENT '原账号',
+              `target_account` char(32) NOT NULL COMMENT '目标账号',
+              `type` int(4) NOT NULL COMMENT '关系类型',
+              `status` tinyint(1) NOT NULL COMMENT '关系状态',
+              `create_date` date NOT NULL COMMENT '创建时间',
+              `update_date` bigint NOT NULL COMMENT '更新时间',
+              PRIMARY KEY (`id`),
+              UNIQUE KEY `UK_IDX_SOURCE_TARGET_ACCOUNT`(source_account, target_account)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='账号关系表';

+ 28 - 0
webchat-admin/src/main/java/com/webchat/admin/controller/AccountManagementController.java

@@ -17,6 +17,8 @@ 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("/admin-service/account")
 public class AccountManagementController {
@@ -24,6 +26,17 @@ public class AccountManagementController {
     @Autowired
     private AccountManagementService accountManagementService;
 
+    /**
+     * 账号分页查询
+     *
+     * @param roleCode
+     * @param userId
+     * @param userName
+     * @param mobile
+     * @param pageNo
+     * @param pageSize
+     * @return
+     */
     @GetMapping("/page")
     public APIPageResponseBean<UserBaseResponseInfoVO> page(
             @RequestParam(value = "roleCode") Integer roleCode,
@@ -37,6 +50,21 @@ public class AccountManagementController {
     }
 
     /**
+     * 账号搜索
+     *
+     * @param query
+     * @param size
+     * @return
+     */
+    @GetMapping("/suggest")
+    public APIResponseBean<List<UserBaseResponseInfoVO>> suggest(
+            @RequestParam(value = "q", required = false) String query,
+            @RequestParam(value = "size", required = false, defaultValue = "10") Integer size) {
+
+        return APIResponseBeanUtil.success(null);
+    }
+
+    /**
      * 创建机器人
      *
      * @return

+ 27 - 5
webchat-admin/src/main/java/com/webchat/admin/service/AccountManagementService.java

@@ -3,18 +3,16 @@ package com.webchat.admin.service;
 import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
-import com.webchat.common.constants.WebConstant;
-import com.webchat.common.enums.RedisKeyEnum;
 import com.webchat.common.exception.BusinessException;
 import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import com.webchat.rmi.user.UserServiceClient;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
 
 @Service
 public class AccountManagementService {
@@ -23,12 +21,36 @@ public class AccountManagementService {
     @Autowired
     private UserServiceClient userServiceClient;
 
+    /**
+     * 分页查询账号信息
+     *
+     * @param userId
+     * @param roleCode
+     * @param userName
+     * @param mobile
+     * @param pageNo
+     * @param pageSize
+     * @return
+     */
     public APIPageResponseBean<UserBaseResponseInfoVO> page(String userId, Integer roleCode, String userName, String mobile, Integer pageNo, Integer pageSize) {
 
         return userServiceClient.page(userId, roleCode, userName, mobile, pageNo, pageSize);
     }
 
     /**
+     * 账号搜索
+     *
+     * @param query
+     * @param size
+     * @return
+     */
+    public List<UserBaseResponseInfoVO> suggest(String query, Integer size) {
+
+        return null;
+    }
+
+
+    /**
      * 创建机器人
      *
      * 复用用户信息表,账号角色为 ROBOT

+ 15 - 4
webchat-client-chat/src/main/java/com/webchat/client/chat/controller/UserController.java → webchat-client-chat/src/main/java/com/webchat/client/chat/controller/AccountController.java

@@ -1,7 +1,7 @@
 package com.webchat.client.chat.controller;
 
 
-import com.webchat.client.chat.service.UserService;
+import com.webchat.client.chat.service.AccountService;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.common.helper.SessionHelper;
@@ -9,15 +9,16 @@ import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
-@RequestMapping("/client-service/chat/user")
-public class UserController {
+@RequestMapping("/client-service/chat/account")
+public class AccountController {
 
 
     @Autowired
-    private UserService userService;
+    private AccountService userService;
 
     @GetMapping("/current/info")
     public APIResponseBean<UserBaseResponseInfoVO> getCurrentUserInfo() {
@@ -25,4 +26,14 @@ public class UserController {
         return APIResponseBeanUtil.success(userService.userInfo(userId));
     }
 
+    /**
+     *
+     * @return
+     */
+    @GetMapping("/query")
+    public APIResponseBean<UserBaseResponseInfoVO> queryAccount(@RequestParam String account) {
+
+        return APIResponseBeanUtil.success(userService.queryAccount(account));
+    }
+
 }

+ 44 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/controller/AccountRelationController.java

@@ -0,0 +1,44 @@
+package com.webchat.client.chat.controller;
+
+
+import com.webchat.client.chat.service.AccountRelationService;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.helper.SessionHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/client-service/chat/account-relation")
+public class AccountRelationController {
+
+
+    @Autowired
+    private AccountRelationService accountRelationService;
+
+    /**
+     * 取消订阅/关注
+     * @param account
+     * @return
+     */
+    @PostMapping("/subscribe/{account}")
+    public APIResponseBean<Boolean> subscribe(@PathVariable String account) {
+        String userId = SessionHelper.getCurrentUserId();
+        return APIResponseBeanUtil.success(accountRelationService.subscribe(userId, account));
+    }
+
+    /**
+     * 取消订阅/关注
+     * @param account
+     * @return
+     */
+    @PostMapping("/unsubscribe/{account}")
+    public APIResponseBean<Boolean> unsubscribe(@PathVariable String account) {
+        String userId = SessionHelper.getCurrentUserId();
+        return APIResponseBeanUtil.success(accountRelationService.unsubscribe(userId, account));
+    }
+
+}

+ 40 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/service/AccountRelationService.java

@@ -0,0 +1,40 @@
+package com.webchat.client.chat.service;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.rmi.user.AccountRelationClient;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AccountRelationService {
+
+
+    @Autowired
+    private AccountRelationClient accountRelationClient;
+
+    public Boolean subscribe(String userId, String account) {
+        APIResponseBean<Boolean> responseBean = accountRelationClient.subscribe(userId, account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getCode(), true);
+        }
+        // TODO
+        return false;
+    }
+
+    /**
+     * 取消订阅/关注
+     * @param account
+     * @return
+     */
+    public Boolean unsubscribe(String userId, String account) {
+        APIResponseBean<Boolean> responseBean = accountRelationClient.unsubscribe(userId, account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getCode(), true);
+        }
+        // TODO
+        return false;
+    }
+}

+ 16 - 1
webchat-client-chat/src/main/java/com/webchat/client/chat/service/UserService.java → webchat-client-chat/src/main/java/com/webchat/client/chat/service/AccountService.java

@@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 @Service
-public class UserService {
+public class AccountService {
 
     @Autowired
     private UserServiceClient userServiceClient;
@@ -24,7 +24,22 @@ public class UserService {
         if (APIResponseBeanUtil.isOk(responseBean)) {
             return responseBean.getData();
         }
+        // TODO
         return null;
     }
 
+    /**
+     * 用户账号查询详情
+     *
+     * @param account
+     * @return
+     */
+    public UserBaseResponseInfoVO queryAccount( String account) {
+        APIResponseBean<UserBaseResponseInfoVO> responseBean = userServiceClient.queryAccount(account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        // TODO
+        return null;
+    }
 }

+ 20 - 0
webchat-common/src/main/java/com/webchat/common/enums/AccountRelationTypeEnum.java

@@ -0,0 +1,20 @@
+package com.webchat.common.enums;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum AccountRelationTypeEnum {
+
+    USER_USER(1, "好友关注"),
+    USER_GROUP(2, "加入群聊"),
+    USER_OFFICIAL(3, "订阅公众号"),
+    USER_ROBOT(4, "添加机器人");
+
+    private Integer type;
+    private String typeName;
+}

+ 5 - 0
webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java

@@ -47,6 +47,11 @@ public enum RedisKeyEnum {
      */
     USER_INFO_CACHE("USER_INFO_CACHE", 7 * 24 * 60 * 60L),
 
+    /**
+     * 用户信息缓存,BY 手机号
+     */
+    USER_INFO_BY_MOBILE_CACHE("USER_INFO_BY_MOBILE_CACHE", 3 * 24 * 60 * 60L),
+
     /***
      * 私信用户列表
      */

+ 7 - 0
webchat-common/src/main/java/com/webchat/common/enums/RoleCodeEnum.java

@@ -24,4 +24,11 @@ public enum RoleCodeEnum {
         this.code = code;
         this.name = name;
     }
+
+    public static boolean isUserRole(Integer roleCode) {
+        if (roleCode == null) {
+            return false;
+        }
+        return USER.code.equals(roleCode) || ADMIN.code.equals(roleCode);
+    }
 }

+ 34 - 0
webchat-remote/src/main/java/com/webchat/rmi/user/AccountRelationClient.java

@@ -0,0 +1,34 @@
+package com.webchat.rmi.user;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@FeignClient(name = "webchat-user-service")
+public interface AccountRelationClient {
+
+    /**
+     * 订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    @PostMapping("/user-service/account-relation/subscribe/{sourceAccount}/{targetAccount}")
+    APIResponseBean<Boolean> subscribe(@PathVariable String sourceAccount,
+                                       @PathVariable String targetAccount);
+
+    /**
+     * 取消订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    @PostMapping("/user-service/account-relation/unsubscribe/{sourceAccount}/{targetAccount}")
+    APIResponseBean<Boolean> unsubscribe(@PathVariable String sourceAccount,
+                                         @PathVariable String targetAccount);
+
+}

+ 10 - 0
webchat-remote/src/main/java/com/webchat/rmi/user/UserServiceClient.java

@@ -103,4 +103,14 @@ public interface UserServiceClient {
      */
     @PostMapping("/user-service/account/batchGet")
     APIResponseBean<Map<String, UserBaseResponseInfoVO>> batchGet(@RequestBody Set<String> userIds);
+
+    /**
+     * 账号搜索
+     * 根据query参数对手机号/账号做完全匹配
+     *
+     * @param account
+     * @return
+     */
+    @GetMapping("/user-service/account/query")
+    APIResponseBean<UserBaseResponseInfoVO> queryAccount(@RequestParam String account);
 }

+ 24 - 0
webchat-user/src/main/java/com/webchat/user/controller/AccountRelationController.java

@@ -0,0 +1,24 @@
+package com.webchat.user.controller;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.rmi.user.AccountRelationClient;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+public class AccountRelationController implements AccountRelationClient {
+
+
+    @Override
+    public APIResponseBean<Boolean> subscribe(@PathVariable String sourceAccount,
+                                              @PathVariable String targetAccount) {
+        return null;
+    }
+
+    @Override
+    public APIResponseBean<Boolean> unsubscribe(@PathVariable String sourceAccount,
+                                                @PathVariable String targetAccount) {
+        return null;
+    }
+}

+ 6 - 0
webchat-user/src/main/java/com/webchat/user/controller/UserServiceController.java

@@ -71,4 +71,10 @@ public class UserServiceController implements UserServiceClient {
         Map<String, UserBaseResponseInfoVO> map = userService.batchGet(userIds);
         return APIResponseBeanUtil.success(map);
     }
+
+    @Override
+    public APIResponseBean<UserBaseResponseInfoVO> queryAccount(String account) {
+
+        return APIResponseBeanUtil.success(userService.queryAccount(account));
+    }
 }

+ 16 - 0
webchat-user/src/main/java/com/webchat/user/repository/dao/IAccountRelationDAO.java

@@ -0,0 +1,16 @@
+package com.webchat.user.repository.dao;
+
+import com.webchat.user.repository.entity.AccountRelationEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface IAccountRelationDAO extends
+        JpaSpecificationExecutor<AccountRelationEntity>,
+        JpaRepository<AccountRelationEntity, Long> {
+
+
+    AccountRelationEntity findAllBySourceAccountAndTargetAccount(String sourceAccount, String targetAccount);
+
+}

+ 43 - 0
webchat-user/src/main/java/com/webchat/user/repository/entity/AccountRelationEntity.java

@@ -0,0 +1,43 @@
+package com.webchat.user.repository.entity;
+
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@Entity
+@Table(name = "web_chat_account_relation")
+public class AccountRelationEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    @Column(name = "source_account")
+    private String sourceAccount;
+
+    @Column(name = "target_account")
+    private String targetAccount;
+
+    /**
+     * @see com.webchat.common.enums.AccountRelationTypeEnum
+     */
+    @Column(name = "type")
+    private Integer type;
+
+    @Column(name = "status")
+    private Boolean status;
+
+    @Column(name = "create_date")
+    private Date createDate;
+
+    @Column(name = "update_date")
+    private Date updateDate;
+}

+ 41 - 0
webchat-user/src/main/java/com/webchat/user/service/UserService.java

@@ -29,6 +29,7 @@ import jakarta.persistence.criteria.Predicate;
 import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -99,6 +100,46 @@ public class UserService {
         return this.batchGetUserInfoFromCache(new ArrayList<>(userIds));
     }
 
+    /**
+     *
+     *
+     * @param account
+     * @return
+     */
+    public UserBaseResponseInfoVO queryAccount(String account) {
+        if (StringUtils.isBlank(account)) {
+            return null;
+        }
+        String cacheKey = RedisKeyEnum.USER_INFO_BY_MOBILE_CACHE.getKey(account);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            if (ObjectUtils.equals(WebConstant.CACHE_NONE, cache)) {
+                return null;
+            }
+            return JsonUtil.fromJson(cache, UserBaseResponseInfoVO.class);
+        }
+        // 刷新BY手机号用户详情缓存
+        return this.refreshUserCacheByMobile(account);
+    }
+
+    /**
+     * 刷新用户详情缓存,基于手机号查询
+     *
+     * @param account
+     * @return
+     */
+    private UserBaseResponseInfoVO refreshUserCacheByMobile(String account) {
+        String cacheKey = RedisKeyEnum.USER_INFO_BY_MOBILE_CACHE.getKey(account);
+        UserEntity userEntity = userDAO.findByMobile(account);
+        if (userEntity == null) {
+            redisService.set(cacheKey, WebConstant.CACHE_NONE, 5 * 60);
+            return null;
+        }
+        UserBaseResponseInfoVO vo = convertBaseVo(userEntity);
+        redisService.set(cacheKey, JsonUtil.toJsonString(vo), RedisKeyEnum.USER_INFO_BY_MOBILE_CACHE.getExpireTime());
+        return vo;
+    }
+
     public APIPageResponseBean<UserBaseResponseInfoVO> page(String userId, Integer roleCode, String userName, String mobile,
                                                             int pageNo, int pageSize) {
 

+ 176 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AbstractAccountRelationService.java

@@ -0,0 +1,176 @@
+package com.webchat.user.service.relation;
+
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.enums.RoleCodeEnum;
+import com.webchat.common.util.ThreadPoolExecutorUtil;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.user.repository.dao.IAccountRelationDAO;
+import com.webchat.user.repository.entity.AccountRelationEntity;
+import com.webchat.user.service.UserService;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+
+import java.util.Date;
+
+public abstract class AbstractAccountRelationService implements AccountRelationValidator, AccountRelationWapper {
+
+    @Autowired
+    private IAccountRelationDAO accountRelationDAO;
+
+    @Autowired
+    private UserService userService;
+
+    /**
+     * 订阅关系类型,需由具体实现类返回
+     *
+     *
+     * @return
+     */
+    protected abstract AccountRelationTypeEnum getRelationType();
+
+    /**
+     * 是否需要异步执行后置处理流程
+     *
+     * @return
+     */
+    protected abstract boolean isAsyncDoAfterComplete();
+
+    /**
+     * 完成核心流程后的业务处理,由各实现类自己实现
+     * 比如:
+     * 1、基于不同关系的缓存构建、刷新
+     */
+    protected abstract void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe);
+
+    /**
+     * 异步执行后置业务处理流程
+     *
+     * @param id
+     * @param sourceAccount
+     * @param targetAccount
+     */
+    protected void asyncDoAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+
+        ThreadPoolExecutorUtil.execute(() -> doAfterComplete(id, sourceAccount, targetAccount, subscribe));
+    }
+
+    private void doExecAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        if (isAsyncDoAfterComplete()) {
+            this.asyncDoAfterComplete(id, sourceAccount, targetAccount, subscribe);
+        } else {
+            this.doAfterComplete(id, sourceAccount, targetAccount, subscribe);
+        }
+    }
+
+    /**
+     * 前置基础校验
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     */
+    @Override
+    public void validateAccountRequest(String sourceAccount, String targetAccount) {
+
+        UserBaseResponseInfoVO sourceAccountInfo = userService.getUserBaseInfoByUserId(sourceAccount);
+        Assert.notNull(sourceAccountInfo, "source account is null!");
+        Assert.isTrue(RoleCodeEnum.isUserRole(sourceAccountInfo.getRoleCode()), "source account role is not support!");
+        UserBaseResponseInfoVO targetAccountInfo = userService.getUserBaseInfoByUserId(targetAccount);
+        Assert.notNull(targetAccountInfo, "target account is null!");
+    }
+
+    @Override
+    public Boolean subscribe(String sourceAccount, String targetAccount) {
+        /**
+         * 前置参数校验
+         */
+        this.validateAccountRequest(sourceAccount, targetAccount);
+        /**
+         * 核心订阅流程处理
+         */
+        Long id = this.doSubscribe(sourceAccount, targetAccount);
+        /**
+         * 执行后置流程
+         */
+        this.doExecAfterComplete(id, sourceAccount, targetAccount, Boolean.TRUE);
+        return true;
+    }
+
+    /**
+     * 执行取消订阅的持久化更新逻辑
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    @Override
+    public Boolean unsubscribe(String sourceAccount, String targetAccount) {
+        /**
+         * 前置参数校验
+         */
+        this.validateAccountRequest(sourceAccount, targetAccount);
+        /**
+         * 核心订阅流程处理
+         */
+        Long id = this.doUnSubscribe(sourceAccount, targetAccount);
+        /**
+         * 执行后置流程
+         */
+        this.doExecAfterComplete(id, sourceAccount, targetAccount, Boolean.FALSE);
+        return true;
+    }
+
+    /**
+     * 执行订阅的持久化更新逻辑
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    private Long doSubscribe(String sourceAccount, String targetAccount) {
+        AccountRelationEntity entity = this.getAccountRelationEntity(sourceAccount, targetAccount);
+        Assert.isTrue(entity == null || ObjectUtils.equals(entity.getStatus(), false), "重复订阅!");
+        if (entity != null) {
+            entity.setStatus(Boolean.TRUE);
+            entity.setUpdateDate(new Date());
+        } else {
+            entity = buildAccountRelationEntity(sourceAccount, targetAccount);
+        }
+        return this.saveAccountRelation2DB(entity);
+    }
+
+    /**
+     * 执行取消订阅的持久化更新逻辑
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    private Long doUnSubscribe(String sourceAccount, String targetAccount) {
+        AccountRelationEntity entity = this.getAccountRelationEntity(sourceAccount, targetAccount);
+        Assert.isTrue(entity != null || ObjectUtils.equals(entity.getStatus(), true), "未订阅,取消操作失败!");
+        entity.setStatus(Boolean.FALSE);
+        entity.setUpdateDate(new Date());
+        return this.saveAccountRelation2DB(entity);
+    }
+
+    private AccountRelationEntity getAccountRelationEntity(String sourceAccount, String targetAccount) {
+
+        return accountRelationDAO.findAllBySourceAccountAndTargetAccount(sourceAccount, targetAccount);
+    }
+
+    private Long saveAccountRelation2DB(AccountRelationEntity entity) {
+
+       return accountRelationDAO.save(entity).getId();
+    }
+
+    private AccountRelationEntity buildAccountRelationEntity(String sourceAccount, String targetAccount) {
+        AccountRelationEntity entity = new AccountRelationEntity();
+        entity.setSourceAccount(sourceAccount);
+        entity.setTargetAccount(targetAccount);
+        entity.setStatus(true);
+        entity.setType(getRelationType().getType());
+        entity.setCreateDate(new Date());
+        return entity;
+    }
+}

+ 43 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationFactory.java

@@ -0,0 +1,43 @@
+package com.webchat.user.service.relation;
+
+
+import com.webchat.common.enums.RoleCodeEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.util.SpringContextUtil;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class AccountRelationFactory {
+
+    private static Map<Integer, AbstractAccountRelationService> services = new HashMap<>();
+
+    @PostConstruct
+    public void init() {
+        this.registryService();
+    }
+
+    /**
+     * 账号关系抽象服务注册
+     *
+     */
+    private void registryService() {
+
+        services.put(RoleCodeEnum.USER.getCode(), SpringContextUtil.getBean(User2UserAccountRelationService.class));
+        services.put(RoleCodeEnum.ADMIN.getCode(), SpringContextUtil.getBean(User2UserAccountRelationService.class));
+        services.put(RoleCodeEnum.ROBOT.getCode(), SpringContextUtil.getBean(User2RobotAccountRelationService.class));
+        services.put(RoleCodeEnum.GROUP.getCode(), SpringContextUtil.getBean(User2GroupAccountRelationService.class));
+        services.put(RoleCodeEnum.PUBLIC_ACCOUNT.getCode(), SpringContextUtil.getBean(User2OfficialAccountRelationService.class));
+    }
+
+    public static AbstractAccountRelationService getService(Integer roleCode) {
+        AbstractAccountRelationService accountRelationService = services.get(roleCode);
+        if (accountRelationService != null) {
+            return accountRelationService;
+        }
+        throw new BusinessException("不支持的关系服务");
+    }
+}

+ 42 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationService.java

@@ -0,0 +1,42 @@
+package com.webchat.user.service.relation;
+
+
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.user.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AccountRelationService {
+
+    @Autowired
+    private UserService userService;
+
+    /**
+     * 订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    public Boolean subscribe(String sourceAccount, String targetAccount) {
+        UserBaseResponseInfoVO targetAccountInfo = userService.getUserBaseInfoByUserId(targetAccount);
+        Integer roleCode = targetAccountInfo.getRoleCode();
+        return AccountRelationFactory.getService(roleCode).subscribe(sourceAccount, targetAccount);
+    }
+
+    /**
+     * 取消订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    Boolean unsubscribe(String sourceAccount, String targetAccount) {
+        UserBaseResponseInfoVO targetAccountInfo = userService.getUserBaseInfoByUserId(targetAccount);
+        Integer roleCode = targetAccountInfo.getRoleCode();
+        return AccountRelationFactory.getService(roleCode).unsubscribe(sourceAccount, targetAccount);
+    }
+
+
+}

+ 15 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationValidator.java

@@ -0,0 +1,15 @@
+package com.webchat.user.service.relation;
+
+public interface AccountRelationValidator {
+
+
+    /**
+     * 校验关系入参
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     */
+    void validateAccountRequest(String sourceAccount, String targetAccount);
+
+
+}

+ 24 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationWapper.java

@@ -0,0 +1,24 @@
+package com.webchat.user.service.relation;
+
+public interface AccountRelationWapper {
+
+
+    /**
+     * 订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    Boolean subscribe(String sourceAccount, String targetAccount);
+
+    /**
+     * 取消订阅/关注
+     *
+     * @param sourceAccount
+     * @param targetAccount
+     * @return
+     */
+    Boolean unsubscribe(String sourceAccount, String targetAccount);
+
+}

+ 27 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/User2GroupAccountRelationService.java

@@ -0,0 +1,27 @@
+package com.webchat.user.service.relation;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class User2GroupAccountRelationService extends AbstractAccountRelationService {
+
+
+    @Override
+    protected AccountRelationTypeEnum getRelationType() {
+
+        return AccountRelationTypeEnum.USER_GROUP;
+    }
+
+    @Override
+    protected boolean isAsyncDoAfterComplete() {
+
+        return true;
+    }
+
+    @Override
+    protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        // TODO
+    }
+}

+ 27 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/User2OfficialAccountRelationService.java

@@ -0,0 +1,27 @@
+package com.webchat.user.service.relation;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class User2OfficialAccountRelationService extends AbstractAccountRelationService {
+
+
+    @Override
+    protected AccountRelationTypeEnum getRelationType() {
+
+        return AccountRelationTypeEnum.USER_OFFICIAL;
+    }
+
+    @Override
+    protected boolean isAsyncDoAfterComplete() {
+
+        return true;
+    }
+
+    @Override
+    protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        // TODO
+    }
+}

+ 26 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/User2RobotAccountRelationService.java

@@ -0,0 +1,26 @@
+package com.webchat.user.service.relation;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class User2RobotAccountRelationService extends AbstractAccountRelationService {
+
+    @Override
+    protected AccountRelationTypeEnum getRelationType() {
+
+        return AccountRelationTypeEnum.USER_ROBOT;
+    }
+
+    @Override
+    protected boolean isAsyncDoAfterComplete() {
+
+        return true;
+    }
+
+    @Override
+    protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        // TODO
+    }
+}

+ 35 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/User2UserAccountRelationService.java

@@ -0,0 +1,35 @@
+package com.webchat.user.service.relation;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class User2UserAccountRelationService extends AbstractAccountRelationService {
+
+
+    @Override
+    protected AccountRelationTypeEnum getRelationType() {
+
+        return AccountRelationTypeEnum.USER_USER;
+    }
+
+    @Override
+    protected boolean isAsyncDoAfterComplete() {
+
+        return true;
+    }
+
+    @Override
+    protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        // TODO
+
+    }
+
+    /**
+     * 基于ws,为targetAccount推送订阅消息红点提醒
+     */
+    private void doNotifyRedPointByWebSocket() {
+
+    }
+}