總結一下本週遇到的問題

李明發表於2022-05-09

在寫區域工作人員端任務管理時需要獲取登陸人員所管理的居民,這時就出現了一個問題——後臺要怎麼實現這些功能呢?
我們可以發現前臺並沒有把當前登入使用者傳給後臺,所以理論上應該是直接從後臺獲取當前登入使用者。
之前專案中在居民管理中也是這種情況,所以就先總結了一下。
把相應的分頁查詢資料傳給後臺C層後,後臺就直接呼叫了residentService.page方法。

  public Page<Resident> page(String name,
                           . . .
                             Pageable pageable) {
    District district = this.filterDistrictOfCurrentUserAccess(districtId);
    Specification<Resident> specification = this.getSpec(name,
       . . .
        beVaccinated);
    Page<Resident> residents = this.residentRepository.findAll(specification, pageable);

    return residents;
  }

我們可以發現這裡通過了filterDistrictOfCurrentUserAccess方法獲取了當前登陸人員的區域ID,並把它加入到綜合查詢條件中。
其中對於綜合查詢中查詢屬於某個地區的居民是這樣做的。

  public static Specification<Resident> belongDistrict(District district) {
    if (district == null ||
        district.getId() == null ||
        district.getType() == null ||
        TYPE_COUNTY.equals(district.getType())) {
      logger.debug("未傳入區域資訊或區域ID為null或傳入了區域為根區域,忽略查詢條件");
      return Specification.where(null);
    }

    return (root, criteriaQuery, criteriaBuilder) -> {
      logger.debug("分別按樓、小區、社群、鄉鎮(預設)進行查詢");
      Join<Resident, Building> buildingJoin = root.join("houses")
          .join("building", JoinType.LEFT);
      Long districtId = district.getId();
      switch (district.getType()) {
        case TYPE_BUILDING:
          return criteriaBuilder.equal(buildingJoin.get("id").as(Long.class), districtId);
        case TYPE_VILLAGE:
          return criteriaBuilder.equal(buildingJoin.join("parent").get("id").as(Long.class),
              districtId);
        case TYPE_COMMUNITY:
          return criteriaBuilder.equal(buildingJoin
                  .join("parent", JoinType.LEFT)
                  .join("parent", JoinType.LEFT)
                  .get("id").as(Long.class),
              districtId);
        default:
          return criteriaBuilder.equal(buildingJoin
                  .join("parent", JoinType.LEFT)
                  .join("parent", JoinType.LEFT)
                  .join("parent", JoinType.LEFT)
                  .get("id").as(Long.class),
              districtId);
      }
    };
  }

看起來很多其實邏輯很簡單,先是判斷傳入的區域資訊是否完整,若不完整則返回null。
之後因為查詢的最小單位是樓所以直接構造了一個從building起始的Join物件

Join<Resident, Building> buildingJoin = root.join("houses")
          .join("building", JoinType.LEFT);

其中的root代表的就是resident,之後進入houses屬性再進入building屬性。

criteriaBuilder.equal(buildingJoin.get("id").as(Long.class), districtId);

再根據傳入的districtId查詢等於buildingId的resident;
之後我們要做的就是根據傳入的district.getType()進行分類討論即如果是小區的話那麼就在此building基礎上獲取parent再進行查詢。

之後我們再來看一下在實際專案中是如何獲取當前登陸使用者並獲取其區域ID的。
先是過濾一下傳入的district,如果傳入了districtId,則看當前登入使用者是否擁有傳入的區域ID的管理許可權。有許可權,返回對應區域;無許可權,返回當前登入使用者所在區域;如果未傳入districtId直接返回登入使用者所在區域。

在此情況下我們先討論如何獲取當前登陸使用者。

public Optional<WebUser> getCurrentLoginWebUser() {
    return this.webUserRepository.findById(this.getCurrentLoginWebUserId()
        .orElseThrow(() -> new AccessDeniedException("當前登入型別不正確或未登入")));
  }

/**
獲取當前登陸使用者ID
**/
public Optional<Long> getCurrentLoginWebUserId() {
    AuthUserDetails authUserDetails = this.userService.getAuthUserDetailWithoutTransaction()
        .orElseThrow(() -> new AccessDeniedException("當前登入型別不正確或未登入"));
    if (authUserDetails instanceof WebUser) {
      return Optional.of(((WebUser) authUserDetails).getId());
    } else {
      return Optional.empty();
    }
  }
  public Optional<AuthUserDetails> getAuthUserDetailWithoutTransaction() {
    logger.debug("根據認證獲取當前登入使用者名稱,並獲取該使用者");
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {
      AuthUserDetails userDetail;
      if (authentication instanceof UsernamePasswordAuthenticationToken) {
        userDetail = (AuthUserDetails) authentication.getPrincipal();
      } else if (authentication instanceof AuthUserDetails) {
        userDetail = (AuthUserDetails) authentication;
      } else if (authentication instanceof AnonymousAuthenticationToken) {
        return Optional.empty();
      } else {
        throw new RuntimeException("獲取型別不正確");
      }
      return Optional.of(userDetail);
    }

    logger.debug("認證使用者在資料庫中不存在");
    return Optional.empty();
  }

根據上述程式碼我們可以發現SpringBoot已經對登陸進行了一部分的封裝,我們在登陸後只需下面這部分程式碼就可獲取當前登陸使用者的資訊

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

之後我們在再根據返回的物件的型別進行分類討論並把它轉化為AuthUserDetails即可。

問題二

使用前後臺登陸後發現選單介面消失,但是手動輸入地址可以進入到想要的頁面.

圖片.png
所以首先要做的就是了解選單是怎麼獲取的,先用前臺測試了一下,發現選單可以正常顯示,
之後再去前臺程式碼中尋找後發現每個選單都配有一個role選項和使用者相對應.

{
          name: '就業人員',
          url: 'employed-persons',
          icon: 'fa fa-user',
          roles: [ROLE_TYPE.volunteer.value, ROLE_TYPE.admin.value]
        }

下面就是獲取選單的方法:

this.userService.currentLoginUser$.subscribe(
        user => {
          console.log(user);
          const roleKeys = user ? user.roles.map(role => role.value) : [];
          subscribe.next(
            MenuService.menus.filter(menu => {
              const menuRoleKeys = menu.roles;
              let found = false;
              menuRoleKeys.forEach(roleKey => {
                if (!found && (roleKeys.indexOf(roleKey) !== -1)) {
                  found = true;
                }
              });
              return found;
            })
          );
        }
      );

先獲取當前登陸使用者的角色陣列,再判斷是否和選單中的角色陣列相匹配再返回查詢結果,於是嘗試著列印獲取到的角色:
圖片.png,發現roles確實為空,之後再去資料庫中檢視後發現角色_使用者的關聯表也確實為空,將上述關係重新新增後恢復正常.

相關文章