Browse Source

新增商家分管账号管理与登录校验

提供主账号管理、平台归属查看与双重状态控制接口。
分管账号登录复用商家会话并校验所属主账号状态,改密或停用时撤销现有会话。
平台通用商家管理入口禁止创建或编辑分管账号。
qmj 1 day ago
parent
commit
7862f9bc9c
20 changed files with 846 additions and 5 deletions
  1. 40 5
      ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java
  2. 38 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountAdminController.java
  3. 267 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountApplicationService.java
  4. 79 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java
  5. 67 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.java
  6. 13 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountCreateRequest.java
  7. 8 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountPasswordRequest.java
  8. 8 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStatusRequest.java
  9. 14 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStoreView.java
  10. 11 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountUpdateRequest.java
  11. 20 0
      ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountView.java
  12. 21 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  13. 21 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  14. 21 0
      ruoyi-admin/src/main/resources/i18n/messages_vi.properties
  15. 21 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  16. 21 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties
  17. 38 0
      ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java
  18. 61 0
      ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountAdminControllerTest.java
  19. 75 0
      ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java
  20. 2 0
      ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml

+ 40 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java

@@ -25,6 +25,7 @@ import com.ruoyi.common.utils.ip.AddressUtils;
 import com.ruoyi.common.utils.ip.IpUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.system.domain.*;
+import com.ruoyi.system.domain.constants.MerchantAccountConstants;
 import com.ruoyi.system.domain.vo.UserDTO;
 import com.ruoyi.app.service.ImAccountService;
 import com.ruoyi.system.mapper.InfoUserMapper;
@@ -34,6 +35,7 @@ import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IUserWalletService;
 import com.ruoyi.system.service.IVipUserService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.utils.MobileSMS;
@@ -100,6 +102,10 @@ public class InfoUserController extends BaseController {
     private InfoUserOauthMapper infoUserOauthMapper;
     @Autowired
     private OAuthVerifyService oauthVerifyService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
     /** 三方登录首登临时凭证 Redis 前缀 */
     private static final String OAUTH_TEMP_PREFIX = "oauth:bind:";
 
@@ -392,7 +398,7 @@ public class InfoUserController extends BaseController {
             QueryWrapper<InfoUser> queryWrapper = new QueryWrapper<>();
             queryWrapper.eq("user_name", userDTO.getUserName());
             queryWrapper.eq("del_flag", "0");
-            queryWrapper.in("user_type", "1", "3", "4");
+            queryWrapper.in("user_type", "1", "3", "4", "5");
             InfoUser user = infoUserService.getOne(queryWrapper);
             if (user == null) {
                 throw new ServiceException(MessageUtils.message("no.user.not.exist"));
@@ -400,19 +406,27 @@ public class InfoUserController extends BaseController {
             String wmima = rsa.decryptByPrivateKey(userDTO.getPassword());
             String nmima = rsa.decryptByPrivateKey(user.getPassword());
             if (wmima.equals(nmima)) {
-                if (!"".equals(userDTO.getCid())) {
-                    InfoUser info = new InfoUser();
-                    info.setUserId(user.getUserId());
+                if (!MerchantAccountConstants.STATUS_ENABLED.equals(user.getStatus())) {
+                    throw new ServiceException(MessageUtils.message("merchant.account.unavailable"));
+                }
+                if (MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(user.getUserType())) {
+                    merchantStoreAccessService.resolve(user.getUserId());
+                }
+                InfoUser info = new InfoUser();
+                info.setUserId(user.getUserId());
+                info.setLastLoginAt(new Date());
+                if (userDTO.getCid() != null && !userDTO.getCid().isBlank()) {
                     info.setCid(userDTO.getCid());
                     info.setCidType(userDTO.getCidType());
                     info.setDeviceToken(userDTO.getDeviceToken());
                     info.setVoIPToken(userDTO.getVoIPToken());
-                    infoUserService.saveOrUpdate(info);
                     user.setCid(userDTO.getCid());
                     user.setCidType(userDTO.getCidType());
                     user.setDeviceToken(userDTO.getDeviceToken());
                     user.setVoIPToken(userDTO.getVoIPToken());
                 }
+                infoUserService.updateInfoUser(info);
+                user.setLastLoginAt(info.getLastLoginAt());
                 // 根据客户端类型(APP端或PC端)选择对应的 token key
                 String tokenKey = getTokenKeyByClientType();
                 //app端
@@ -546,6 +560,17 @@ public class InfoUserController extends BaseController {
         return isAppClient() ? CacheConstants.SH_APP_TOKEN_KEY : CacheConstants.SH_PC_TOKEN_KEY;
     }
 
+    /**
+     * 商家端退出登录,仅撤销当前 JWT 对应的会话。
+     */
+    @Anonymous
+    @Auth
+    @PostMapping("/merchantLogout")
+    public AjaxResult merchantLogout(@RequestHeader String token) {
+        merchantTokenSessionService.logoutCurrent(token);
+        return success();
+    }
+
 
     private void createUserWallet(Long userId) {
         try {
@@ -1168,6 +1193,9 @@ public class InfoUserController extends BaseController {
     @PostMapping
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult add(@RequestBody InfoUser infoUser) {
+        if (MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(infoUser.getUserType())) {
+            return error(MessageUtils.message("merchant.subaccount.platform.managed"));
+        }
         if (!normalizeAuditRejectReason(infoUser)) {
             return error(MessageUtils.message("no.user.audit.reject.reason.required"));
         }
@@ -1192,6 +1220,13 @@ public class InfoUserController extends BaseController {
     @Log(title = "用户信息", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody InfoUser infoUser) {
+        InfoUser existing = infoUser.getUserId() == null ? null
+                : infoUserService.selectInfoUserByUserId(infoUser.getUserId());
+        if (MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(infoUser.getUserType())
+                || (existing != null
+                && MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(existing.getUserType()))) {
+            return error(MessageUtils.message("merchant.subaccount.platform.managed"));
+        }
         if (!normalizeAuditRejectReason(infoUser)) {
             return error(MessageUtils.message("no.user.audit.reject.reason.required"));
         }

+ 38 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountAdminController.java

@@ -0,0 +1,38 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.user.dto.MerchantSubaccountStatusRequest;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import org.springframework.security.access.prepost.PreAuthorize;
+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("/infouser/merchant-subaccounts")
+public class MerchantSubaccountAdminController extends BaseController {
+
+    private final MerchantSubaccountApplicationService applicationService;
+
+    public MerchantSubaccountAdminController(MerchantSubaccountApplicationService applicationService) {
+        this.applicationService = applicationService;
+    }
+
+    @PreAuthorize("@ss.hasPermi('infouser:user:list')")
+    @GetMapping
+    public AjaxResult list(@RequestParam Long merchantUserId) {
+        return success(applicationService.listForPlatform(merchantUserId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('infouser:user:edit')")
+    @PutMapping("/{subaccountUserId}/platform-status")
+    public AjaxResult updatePlatformStatus(@PathVariable Long subaccountUserId,
+                                           @RequestBody MerchantSubaccountStatusRequest request) {
+        applicationService.updatePlatformStatus(subaccountUserId, request);
+        return success();
+    }
+}

+ 267 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountApplicationService.java

@@ -0,0 +1,267 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.user.dto.MerchantSubaccountCreateRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountPasswordRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountStatusRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountStoreView;
+import com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountView;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosStore;
+import com.ruoyi.system.domain.constants.MerchantAccountConstants;
+import com.ruoyi.system.mapper.PosStoreMapper;
+import com.ruoyi.system.service.IInfoUserService;
+import com.ruoyi.system.service.IMerchantSubaccountStoreService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+@Service
+public class MerchantSubaccountApplicationService {
+
+    private final IInfoUserService infoUserService;
+    private final IMerchantSubaccountStoreService relationService;
+    private final MerchantStoreAccessService accessService;
+    private final PosStoreMapper posStoreMapper;
+    private final MerchantTokenSessionService tokenSessionService;
+
+    public MerchantSubaccountApplicationService(IInfoUserService infoUserService,
+                                                IMerchantSubaccountStoreService relationService,
+                                                MerchantStoreAccessService accessService,
+                                                PosStoreMapper posStoreMapper,
+                                                MerchantTokenSessionService tokenSessionService) {
+        this.infoUserService = infoUserService;
+        this.relationService = relationService;
+        this.accessService = accessService;
+        this.posStoreMapper = posStoreMapper;
+        this.tokenSessionService = tokenSessionService;
+    }
+
+    public List<MerchantSubaccountView> listForOwner(Long ownerUserId) {
+        accessService.requireOwner(ownerUserId);
+        return listOwnedSubaccounts(ownerUserId);
+    }
+
+    public List<MerchantSubaccountView> listForPlatform(Long ownerUserId) {
+        InfoUser owner = infoUserService.selectInfoUserByUserId(ownerUserId);
+        if (owner == null || !MerchantAccountConstants.OWNER_USER_TYPE.equals(owner.getUserType())
+                || !MerchantAccountConstants.NOT_DELETED.equals(owner.getDelFlag())) {
+            throw error("merchant.owner.not.found");
+        }
+        return listOwnedSubaccounts(ownerUserId);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public MerchantSubaccountView create(Long ownerUserId, MerchantSubaccountCreateRequest request) {
+        accessService.requireOwner(ownerUserId);
+        if (request == null) {
+            throw error("merchant.subaccount.request.required");
+        }
+        String phone = normalizePhone(request.getPhone());
+        String name = normalizeRequired(request.getName(), "merchant.subaccount.name.required");
+        String password = normalizeRequired(request.getPassword(), "merchant.subaccount.password.required");
+        List<Long> storeIds = validateStores(ownerUserId, request.getStoreIds());
+        if (infoUserService.getinfouserName(phone) != null || infoUserService.getinfoPhone(phone) != null) {
+            throw error("merchant.subaccount.phone.exists");
+        }
+
+        InfoUser user = new InfoUser();
+        user.setUserName(phone);
+        user.setPhone(phone);
+        user.setNickName(name);
+        user.setPassword(password);
+        user.setUserType(MerchantAccountConstants.SUBACCOUNT_USER_TYPE);
+        user.setMerchantOwnerId(ownerUserId);
+        user.setStatus(MerchantAccountConstants.STATUS_ENABLED);
+        user.setSubaccountStatus(MerchantAccountConstants.STATUS_ENABLED);
+        user.setAuditStatus("1");
+        user.setDelFlag(MerchantAccountConstants.NOT_DELETED);
+        if (infoUserService.insertInfoUser(user) != 1 || user.getUserId() == null) {
+            throw error("merchant.subaccount.create.failed");
+        }
+        relationService.replaceRelations(user.getUserId(), storeIds);
+        return toView(user, ownerUserId);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public MerchantSubaccountView update(Long ownerUserId, Long subaccountUserId,
+                                         MerchantSubaccountUpdateRequest request) {
+        accessService.requireOwner(ownerUserId);
+        InfoUser target = requireOwnedSubaccount(ownerUserId, subaccountUserId);
+        if (request == null) {
+            throw error("merchant.subaccount.request.required");
+        }
+        String name = normalizeRequired(request.getName(), "merchant.subaccount.name.required");
+        List<Long> storeIds = validateStores(ownerUserId, request.getStoreIds());
+
+        InfoUser update = new InfoUser();
+        update.setUserId(target.getUserId());
+        update.setNickName(name);
+        if (infoUserService.updateInfoUser(update) != 1) {
+            throw error("merchant.subaccount.update.failed");
+        }
+        relationService.replaceRelations(target.getUserId(), storeIds);
+        target.setNickName(name);
+        return toView(target, ownerUserId);
+    }
+
+    public void updatePassword(Long ownerUserId, Long subaccountUserId,
+                               MerchantSubaccountPasswordRequest request) {
+        accessService.requireOwner(ownerUserId);
+        InfoUser target = requireOwnedSubaccount(ownerUserId, subaccountUserId);
+        String password = normalizeRequired(request == null ? null : request.getPassword(),
+                "merchant.subaccount.password.required");
+        InfoUser update = new InfoUser();
+        update.setUserId(target.getUserId());
+        update.setPassword(password);
+        if (infoUserService.updateInfoUser(update) != 1) {
+            throw error("merchant.subaccount.update.failed");
+        }
+        tokenSessionService.revokeAll(target.getUserId());
+    }
+
+    public void updateOwnerStatus(Long ownerUserId, Long subaccountUserId,
+                                  MerchantSubaccountStatusRequest request) {
+        accessService.requireOwner(ownerUserId);
+        InfoUser target = requireOwnedSubaccount(ownerUserId, subaccountUserId);
+        boolean enabled = requireEnabled(request);
+        InfoUser update = new InfoUser();
+        update.setUserId(target.getUserId());
+        update.setSubaccountStatus(enabled
+                ? MerchantAccountConstants.STATUS_ENABLED : MerchantAccountConstants.STATUS_DISABLED);
+        if (infoUserService.updateInfoUser(update) != 1) {
+            throw error("merchant.subaccount.update.failed");
+        }
+        if (!enabled) {
+            tokenSessionService.revokeAll(target.getUserId());
+        }
+    }
+
+    public void updatePlatformStatus(Long subaccountUserId, MerchantSubaccountStatusRequest request) {
+        InfoUser target = requireSubaccount(subaccountUserId);
+        boolean enabled = requireEnabled(request);
+        InfoUser update = new InfoUser();
+        update.setUserId(target.getUserId());
+        update.setStatus(enabled
+                ? MerchantAccountConstants.STATUS_ENABLED : MerchantAccountConstants.STATUS_DISABLED);
+        if (infoUserService.updateInfoUser(update) != 1) {
+            throw error("merchant.subaccount.update.failed");
+        }
+        if (!enabled) {
+            tokenSessionService.revokeAll(target.getUserId());
+        }
+    }
+
+    private List<MerchantSubaccountView> listOwnedSubaccounts(Long ownerUserId) {
+        InfoUser filter = new InfoUser();
+        filter.setUserType(MerchantAccountConstants.SUBACCOUNT_USER_TYPE);
+        filter.setMerchantOwnerId(ownerUserId);
+        filter.setDelFlag(MerchantAccountConstants.NOT_DELETED);
+        return infoUserService.selectInfoUserList(filter).stream()
+                .map(user -> toView(user, ownerUserId))
+                .toList();
+    }
+
+    private MerchantSubaccountView toView(InfoUser user, Long ownerUserId) {
+        MerchantSubaccountView view = new MerchantSubaccountView();
+        view.setUserId(user.getUserId());
+        view.setName(firstNonBlank(user.getNickName(), user.getFullName(), user.getUserName()));
+        view.setPhone(user.getPhone());
+        view.setMerchantOwnerId(ownerUserId);
+        view.setOwnerEnabled(MerchantAccountConstants.STATUS_ENABLED.equals(user.getSubaccountStatus()));
+        view.setPlatformEnabled(MerchantAccountConstants.STATUS_ENABLED.equals(user.getStatus()));
+        view.setOnline(tokenSessionService.isOnline(user.getUserId()));
+        view.setLastLoginAt(user.getLastLoginAt());
+        view.setCreatedAt(user.getCreatedAt());
+        List<MerchantSubaccountStoreView> stores = new ArrayList<>();
+        for (Long storeId : relationService.selectStoreIdsBySubaccountUserId(user.getUserId())) {
+            PosStore store = posStoreMapper.selectPosStoreById(storeId);
+            if (store != null && Objects.equals(ownerUserId, store.getUserId())
+                    && MerchantAccountConstants.NOT_DELETED.equals(store.getDelFlag())) {
+                stores.add(new MerchantSubaccountStoreView(storeId, store.getPosName()));
+            }
+        }
+        view.setStores(stores);
+        return view;
+    }
+
+    private List<Long> validateStores(Long ownerUserId, List<Long> requested) {
+        if (requested == null || requested.isEmpty()) {
+            throw error("merchant.subaccount.store.required");
+        }
+        Set<Long> storeIds = new LinkedHashSet<>();
+        for (Long storeId : requested) {
+            if (storeId == null) {
+                throw error("merchant.store.access.denied");
+            }
+            storeIds.add(storeId);
+        }
+        if (!accessService.getAccessibleStoreIds(ownerUserId).containsAll(storeIds)) {
+            throw error("merchant.store.access.denied");
+        }
+        return List.copyOf(storeIds);
+    }
+
+    private InfoUser requireOwnedSubaccount(Long ownerUserId, Long subaccountUserId) {
+        InfoUser target = requireSubaccount(subaccountUserId);
+        if (!Objects.equals(ownerUserId, target.getMerchantOwnerId())) {
+            throw error("merchant.subaccount.access.denied");
+        }
+        return target;
+    }
+
+    private InfoUser requireSubaccount(Long subaccountUserId) {
+        InfoUser target = subaccountUserId == null ? null
+                : infoUserService.selectInfoUserByUserId(subaccountUserId);
+        if (target == null
+                || !MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(target.getUserType())
+                || !MerchantAccountConstants.NOT_DELETED.equals(target.getDelFlag())) {
+            throw error("merchant.subaccount.not.found");
+        }
+        return target;
+    }
+
+    private boolean requireEnabled(MerchantSubaccountStatusRequest request) {
+        if (request == null || request.getEnabled() == null) {
+            throw error("merchant.subaccount.status.required");
+        }
+        return request.getEnabled();
+    }
+
+    private String normalizePhone(String value) {
+        String phone = normalizeRequired(value, "merchant.subaccount.phone.required")
+                .replaceAll("\\s+", "");
+        if (phone.length() > 32) {
+            throw error("merchant.subaccount.phone.invalid");
+        }
+        return phone;
+    }
+
+    private String normalizeRequired(String value, String key) {
+        if (value == null || value.isBlank()) {
+            throw error(key);
+        }
+        return value.strip();
+    }
+
+    private String firstNonBlank(String... values) {
+        for (String value : values) {
+            if (value != null && !value.isBlank()) {
+                return value;
+            }
+        }
+        return "";
+    }
+
+    private ServiceException error(String key) {
+        return new ServiceException(MessageUtils.message(key));
+    }
+}

+ 79 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java

@@ -0,0 +1,79 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.user.dto.MerchantSubaccountCreateRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountPasswordRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountStatusRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.system.utils.Auth;
+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.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/merchant/subaccounts")
+public class MerchantSubaccountController extends BaseController {
+
+    private final MerchantSubaccountApplicationService applicationService;
+    private final MerchantTokenSessionService tokenSessionService;
+
+    public MerchantSubaccountController(MerchantSubaccountApplicationService applicationService,
+                                        MerchantTokenSessionService tokenSessionService) {
+        this.applicationService = applicationService;
+        this.tokenSessionService = tokenSessionService;
+    }
+
+    @Anonymous
+    @Auth
+    @GetMapping
+    public AjaxResult list(@RequestHeader String token) {
+        return success(applicationService.listForOwner(tokenSessionService.requireUserId(token)));
+    }
+
+    @Anonymous
+    @Auth
+    @PostMapping
+    public AjaxResult create(@RequestHeader String token,
+                             @RequestBody MerchantSubaccountCreateRequest request) {
+        return success(applicationService.create(tokenSessionService.requireUserId(token), request));
+    }
+
+    @Anonymous
+    @Auth
+    @PutMapping("/{subaccountUserId}")
+    public AjaxResult update(@RequestHeader String token,
+                             @PathVariable Long subaccountUserId,
+                             @RequestBody MerchantSubaccountUpdateRequest request) {
+        return success(applicationService.update(tokenSessionService.requireUserId(token),
+                subaccountUserId, request));
+    }
+
+    @Anonymous
+    @Auth
+    @PutMapping("/{subaccountUserId}/password")
+    public AjaxResult updatePassword(@RequestHeader String token,
+                                     @PathVariable Long subaccountUserId,
+                                     @RequestBody MerchantSubaccountPasswordRequest request) {
+        applicationService.updatePassword(tokenSessionService.requireUserId(token),
+                subaccountUserId, request);
+        return success();
+    }
+
+    @Anonymous
+    @Auth
+    @PutMapping("/{subaccountUserId}/status")
+    public AjaxResult updateStatus(@RequestHeader String token,
+                                   @PathVariable Long subaccountUserId,
+                                   @RequestBody MerchantSubaccountStatusRequest request) {
+        applicationService.updateOwnerStatus(tokenSessionService.requireUserId(token),
+                subaccountUserId, request);
+        return success();
+    }
+}

+ 67 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.java

@@ -0,0 +1,67 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.utils.JwtUtil;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+
+@Service
+public class MerchantTokenSessionService {
+
+    private final RedisCache redisCache;
+
+    public MerchantTokenSessionService(RedisCache redisCache) {
+        this.redisCache = redisCache;
+    }
+
+    public Long requireUserId(String token) {
+        if (token == null || token.isBlank()) {
+            throw unauthorized();
+        }
+        JwtUtil jwtUtil = new JwtUtil();
+        String userId = jwtUtil.getusid(token);
+        String jti = jwtUtil.getJti(token);
+        if (userId == null || jti == null || !Boolean.TRUE.equals(redisCache.hasKey(jti))) {
+            throw unauthorized();
+        }
+        try {
+            return Long.valueOf(userId);
+        } catch (NumberFormatException exception) {
+            throw unauthorized();
+        }
+    }
+
+    public void logoutCurrent(String token) {
+        requireUserId(token);
+        String jti = new JwtUtil().getJti(token);
+        if (jti != null) {
+            redisCache.deleteObject(jti);
+        }
+    }
+
+    public void revokeAll(Long userId) {
+        if (userId == null) {
+            return;
+        }
+        redisCache.deleteKeys(CacheConstants.SH_APP_TOKEN_KEY + userId + ":*");
+        redisCache.deleteKeys(CacheConstants.SH_PC_TOKEN_KEY + userId + ":*");
+    }
+
+    public boolean isOnline(Long userId) {
+        return hasSession(CacheConstants.SH_APP_TOKEN_KEY, userId)
+                || hasSession(CacheConstants.SH_PC_TOKEN_KEY, userId);
+    }
+
+    private boolean hasSession(String prefix, Long userId) {
+        Collection<String> keys = redisCache.keys(prefix + userId + ":*");
+        return keys != null && !keys.isEmpty();
+    }
+
+    private ServiceException unauthorized() {
+        return new ServiceException(MessageUtils.message("merchant.session.invalid"));
+    }
+}

+ 13 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountCreateRequest.java

@@ -0,0 +1,13 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MerchantSubaccountCreateRequest {
+    private String phone;
+    private String name;
+    private String password;
+    private List<Long> storeIds;
+}

+ 8 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountPasswordRequest.java

@@ -0,0 +1,8 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+@Data
+public class MerchantSubaccountPasswordRequest {
+    private String password;
+}

+ 8 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStatusRequest.java

@@ -0,0 +1,8 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+@Data
+public class MerchantSubaccountStatusRequest {
+    private Boolean enabled;
+}

+ 14 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStoreView.java

@@ -0,0 +1,14 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+@Data
+public class MerchantSubaccountStoreView {
+    private Long storeId;
+    private String storeName;
+
+    public MerchantSubaccountStoreView(Long storeId, String storeName) {
+        this.storeId = storeId;
+        this.storeName = storeName;
+    }
+}

+ 11 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountUpdateRequest.java

@@ -0,0 +1,11 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MerchantSubaccountUpdateRequest {
+    private String name;
+    private List<Long> storeIds;
+}

+ 20 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountView.java

@@ -0,0 +1,20 @@
+package com.ruoyi.app.user.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class MerchantSubaccountView {
+    private Long userId;
+    private String name;
+    private String phone;
+    private Long merchantOwnerId;
+    private boolean ownerEnabled;
+    private boolean platformEnabled;
+    private boolean online;
+    private Date lastLoginAt;
+    private Date createdAt;
+    private List<MerchantSubaccountStoreView> stores;
+}

+ 21 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -262,3 +262,24 @@ no.order.paytype.not.cash=Đơn hàng này không phải thanh toán tiền mặ
 no.order.cash.already.paid=Đơn hàng này đã được xác nhận thu tiền
 no.order.cash.cancelled=Đơn hàng đã bị hủy, không thể xác nhận thu tiền
 no.user.audit.reject.reason.required=审核不通过时必须填写审核不通过原因
+merchant.account.unavailable=Merchant account is unavailable
+merchant.owner.unavailable=Merchant owner account is unavailable
+merchant.owner.required=Only the merchant owner account can perform this operation
+merchant.owner.not.found=Merchant owner account was not found
+merchant.store.access.denied=You do not have permission to access this store
+merchant.order.not.found=Order was not found
+merchant.food.not.found=Product was not found
+merchant.session.invalid=Merchant session is invalid, please sign in again
+merchant.subaccount.request.required=Request data is required
+merchant.subaccount.name.required=Subaccount name is required
+merchant.subaccount.phone.required=Subaccount phone number is required
+merchant.subaccount.phone.invalid=Subaccount phone number is invalid
+merchant.subaccount.phone.exists=This phone number is already in use
+merchant.subaccount.password.required=Subaccount password is required
+merchant.subaccount.store.required=Select at least one responsible store
+merchant.subaccount.status.required=Subaccount status is required
+merchant.subaccount.not.found=Subaccount was not found
+merchant.subaccount.access.denied=You cannot manage this subaccount
+merchant.subaccount.create.failed=Failed to create subaccount
+merchant.subaccount.update.failed=Failed to update subaccount
+merchant.subaccount.platform.managed=Subaccounts can only be managed from the dedicated merchant subaccount entry

+ 21 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -265,3 +265,24 @@ no.order.paytype.not.cash=This order is not a cash payment order
 no.order.cash.already.paid=Cash payment for this order has already been confirmed
 no.order.cash.cancelled=The order has been cancelled; cash payment cannot be confirmed
 no.user.audit.reject.reason.required=A rejection reason is required when the audit is rejected
+merchant.account.unavailable=Merchant account is unavailable
+merchant.owner.unavailable=Merchant owner account is unavailable
+merchant.owner.required=Only the merchant owner account can perform this operation
+merchant.owner.not.found=Merchant owner account was not found
+merchant.store.access.denied=You do not have permission to access this store
+merchant.order.not.found=Order was not found
+merchant.food.not.found=Product was not found
+merchant.session.invalid=Merchant session is invalid, please sign in again
+merchant.subaccount.request.required=Request data is required
+merchant.subaccount.name.required=Subaccount name is required
+merchant.subaccount.phone.required=Subaccount phone number is required
+merchant.subaccount.phone.invalid=Subaccount phone number is invalid
+merchant.subaccount.phone.exists=This phone number is already in use
+merchant.subaccount.password.required=Subaccount password is required
+merchant.subaccount.store.required=Select at least one responsible store
+merchant.subaccount.status.required=Subaccount status is required
+merchant.subaccount.not.found=Subaccount was not found
+merchant.subaccount.access.denied=You cannot manage this subaccount
+merchant.subaccount.create.failed=Failed to create subaccount
+merchant.subaccount.update.failed=Failed to update subaccount
+merchant.subaccount.platform.managed=Subaccounts can only be managed from the dedicated merchant subaccount entry

+ 21 - 0
ruoyi-admin/src/main/resources/i18n/messages_vi.properties

@@ -265,3 +265,24 @@ no.order.paytype.not.cash=Đơn hàng này không phải thanh toán tiền mặ
 no.order.cash.already.paid=Đơn hàng này đã được xác nhận thu tiền
 no.order.cash.cancelled=Đơn hàng đã bị hủy, không thể xác nhận thu tiền
 no.user.audit.reject.reason.required=Phải nhập lý do khi kết quả xét duyệt không được thông qua
+merchant.account.unavailable=Tài khoản cửa hàng không khả dụng
+merchant.owner.unavailable=Tài khoản chủ cửa hàng không khả dụng
+merchant.owner.required=Chỉ tài khoản chủ cửa hàng mới được thực hiện thao tác này
+merchant.owner.not.found=Không tìm thấy tài khoản chủ cửa hàng
+merchant.store.access.denied=Bạn không có quyền truy cập cửa hàng này
+merchant.order.not.found=Không tìm thấy đơn hàng
+merchant.food.not.found=Không tìm thấy sản phẩm
+merchant.session.invalid=Phiên đăng nhập cửa hàng đã hết hiệu lực, vui lòng đăng nhập lại
+merchant.subaccount.request.required=Dữ liệu yêu cầu không được để trống
+merchant.subaccount.name.required=Tên tài khoản phụ không được để trống
+merchant.subaccount.phone.required=Số điện thoại tài khoản phụ không được để trống
+merchant.subaccount.phone.invalid=Số điện thoại tài khoản phụ không hợp lệ
+merchant.subaccount.phone.exists=Số điện thoại này đã được sử dụng
+merchant.subaccount.password.required=Mật khẩu tài khoản phụ không được để trống
+merchant.subaccount.store.required=Vui lòng chọn ít nhất một cửa hàng phụ trách
+merchant.subaccount.status.required=Trạng thái tài khoản phụ không được để trống
+merchant.subaccount.not.found=Không tìm thấy tài khoản phụ
+merchant.subaccount.access.denied=Bạn không có quyền quản lý tài khoản phụ này
+merchant.subaccount.create.failed=Tạo tài khoản phụ thất bại
+merchant.subaccount.update.failed=Cập nhật tài khoản phụ thất bại
+merchant.subaccount.platform.managed=Tài khoản phụ chỉ được quản lý tại mục quản lý chuyên dụng

+ 21 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -266,3 +266,24 @@ no.order.paytype.not.cash=该订单不是现金支付订单
 no.order.cash.already.paid=该订单已确认收款
 no.order.cash.cancelled=订单已取消,不能确认收款
 no.user.audit.reject.reason.required=审核不通过时必须填写审核不通过原因
+merchant.account.unavailable=商家账号不可用
+merchant.owner.unavailable=所属商家主账号不可用
+merchant.owner.required=仅商家主账号可执行此操作
+merchant.owner.not.found=商家主账号不存在
+merchant.store.access.denied=无权访问该店铺
+merchant.order.not.found=订单不存在
+merchant.food.not.found=商品不存在
+merchant.session.invalid=商家登录已失效,请重新登录
+merchant.subaccount.request.required=请求数据不能为空
+merchant.subaccount.name.required=分管账号姓名不能为空
+merchant.subaccount.phone.required=分管账号手机号不能为空
+merchant.subaccount.phone.invalid=分管账号手机号格式无效
+merchant.subaccount.phone.exists=该手机号已被使用
+merchant.subaccount.password.required=分管账号密码不能为空
+merchant.subaccount.store.required=请至少选择一个负责店铺
+merchant.subaccount.status.required=分管账号状态不能为空
+merchant.subaccount.not.found=分管账号不存在
+merchant.subaccount.access.denied=无权管理该分管账号
+merchant.subaccount.create.failed=创建分管账号失败
+merchant.subaccount.update.failed=更新分管账号失败
+merchant.subaccount.platform.managed=分管账号只能通过专用入口管理

+ 21 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties

@@ -266,3 +266,24 @@ no.order.paytype.not.cash=該訂單不是現金支付訂單
 no.order.cash.already.paid=該訂單已確認收款
 no.order.cash.cancelled=訂單已取消,不能確認收款
 no.user.audit.reject.reason.required=審核不通過時必須填寫審核不通過原因
+merchant.account.unavailable=商家帳號不可用
+merchant.owner.unavailable=所屬商家主帳號不可用
+merchant.owner.required=僅商家主帳號可執行此操作
+merchant.owner.not.found=商家主帳號不存在
+merchant.store.access.denied=無權存取該店鋪
+merchant.order.not.found=訂單不存在
+merchant.food.not.found=商品不存在
+merchant.session.invalid=商家登入已失效,請重新登入
+merchant.subaccount.request.required=請求資料不可為空
+merchant.subaccount.name.required=分管帳號姓名不可為空
+merchant.subaccount.phone.required=分管帳號手機號碼不可為空
+merchant.subaccount.phone.invalid=分管帳號手機號碼格式無效
+merchant.subaccount.phone.exists=該手機號碼已被使用
+merchant.subaccount.password.required=分管帳號密碼不可為空
+merchant.subaccount.store.required=請至少選擇一個負責店鋪
+merchant.subaccount.status.required=分管帳號狀態不可為空
+merchant.subaccount.not.found=分管帳號不存在
+merchant.subaccount.access.denied=無權管理該分管帳號
+merchant.subaccount.create.failed=建立分管帳號失敗
+merchant.subaccount.update.failed=更新分管帳號失敗
+merchant.subaccount.platform.managed=分管帳號只能透過專用入口管理

+ 38 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java

@@ -47,6 +47,7 @@ class InfoUserControllerTest {
     private IPosOrderService posOrderService;
     private IInfoUserService infoUserService;
     private IUserWalletService userWalletService;
+    private MerchantTokenSessionService merchantTokenSessionService;
     private static ConfigurableListableBeanFactory originalBeanFactory;
 
     @BeforeAll
@@ -62,6 +63,8 @@ class InfoUserControllerTest {
         messageSource.addMessage("no.user.audit.reject.reason.required", Locale.getDefault(),
                 "审核不通过时必须填写审核不通过原因");
         messageSource.addMessage("no.action.success", Locale.getDefault(), "操作成功");
+        messageSource.addMessage("merchant.subaccount.platform.managed", Locale.getDefault(),
+                "分管账号只能通过专用入口管理");
         beanFactory.registerSingleton("messageSource", messageSource);
         new SpringUtils().postProcessBeanFactory(beanFactory);
     }
@@ -77,9 +80,11 @@ class InfoUserControllerTest {
         posOrderService = mock(IPosOrderService.class);
         infoUserService = mock(IInfoUserService.class);
         userWalletService = mock(IUserWalletService.class);
+        merchantTokenSessionService = mock(MerchantTokenSessionService.class);
         ReflectionTestUtils.setField(controller, "posOrderService", posOrderService);
         ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
         ReflectionTestUtils.setField(controller, "userWalletService", userWalletService);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", merchantTokenSessionService);
     }
 
     @Test
@@ -217,6 +222,39 @@ class InfoUserControllerTest {
         verify(infoUserService).updateInfoUser(user);
     }
 
+    @Test
+    void platformGenericAddCannotCreateMerchantSubaccount() {
+        InfoUser user = new InfoUser();
+        user.setUserType("5");
+
+        AjaxResult result = controller.add(user);
+
+        assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
+        verify(infoUserService, never()).insertInfoUser(any(InfoUser.class));
+    }
+
+    @Test
+    void platformGenericEditCannotModifyMerchantSubaccount() {
+        InfoUser existing = new InfoUser();
+        existing.setUserId(55L);
+        existing.setUserType("5");
+        when(infoUserService.selectInfoUserByUserId(55L)).thenReturn(existing);
+        InfoUser request = new InfoUser();
+        request.setUserId(55L);
+
+        AjaxResult result = controller.edit(request);
+
+        assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
+        verify(infoUserService, never()).updateInfoUser(request);
+    }
+
+    @Test
+    void merchantLogoutRevokesOnlyCurrentTokenSession() {
+        controller.merchantLogout("merchant-token");
+
+        verify(merchantTokenSessionService).logoutCurrent("merchant-token");
+    }
+
     private String tokenFor(Long userId) {
         return JwtUtil.setToken(String.valueOf(userId), "test-user");
     }

+ 61 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountAdminControllerTest.java

@@ -0,0 +1,61 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.user.dto.MerchantSubaccountStatusRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountView;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MerchantSubaccountAdminControllerTest {
+
+    private MerchantSubaccountApplicationService applicationService;
+    private MerchantSubaccountAdminController controller;
+    private MockedStatic<MessageUtils> messages;
+
+    @BeforeEach
+    void setUp() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        applicationService = mock(MerchantSubaccountApplicationService.class);
+        controller = new MerchantSubaccountAdminController(applicationService);
+    }
+
+    @AfterEach
+    void tearDown() {
+        messages.close();
+    }
+
+    @Test
+    void listsOnlySubaccountsResolvedForSpecifiedOwner() {
+        MerchantSubaccountView view = new MerchantSubaccountView();
+        view.setUserId(8L);
+        when(applicationService.listForPlatform(1L)).thenReturn(List.of(view));
+
+        AjaxResult result = controller.list(1L);
+
+        assertEquals(List.of(view), result.get(AjaxResult.DATA_TAG));
+        verify(applicationService).listForPlatform(1L);
+    }
+
+    @Test
+    void platformStatusEndpointDoesNotChangeOwnerStatus() {
+        MerchantSubaccountStatusRequest request = new MerchantSubaccountStatusRequest();
+        request.setEnabled(false);
+
+        controller.updatePlatformStatus(8L, request);
+
+        verify(applicationService).updatePlatformStatus(8L, request);
+    }
+}

+ 75 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java

@@ -0,0 +1,75 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.user.dto.MerchantSubaccountCreateRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountPasswordRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountStatusRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest;
+import com.ruoyi.app.user.dto.MerchantSubaccountView;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MerchantSubaccountControllerTest {
+
+    private MerchantSubaccountApplicationService applicationService;
+    private MerchantTokenSessionService tokenSessionService;
+    private MerchantSubaccountController controller;
+    private MockedStatic<MessageUtils> messages;
+
+    @BeforeEach
+    void setUp() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        applicationService = mock(MerchantSubaccountApplicationService.class);
+        tokenSessionService = mock(MerchantTokenSessionService.class);
+        controller = new MerchantSubaccountController(applicationService, tokenSessionService);
+        when(tokenSessionService.requireUserId("token")).thenReturn(10L);
+    }
+
+    @AfterEach
+    void tearDown() {
+        messages.close();
+    }
+
+    @Test
+    void listUsesAuthenticatedMerchantInsteadOfRequestOwner() {
+        MerchantSubaccountView view = new MerchantSubaccountView();
+        view.setUserId(20L);
+        when(applicationService.listForOwner(10L)).thenReturn(List.of(view));
+
+        AjaxResult result = controller.list("token");
+
+        assertEquals(List.of(view), result.get(AjaxResult.DATA_TAG));
+        verify(applicationService).listForOwner(10L);
+    }
+
+    @Test
+    void mutationsAlwaysUseAuthenticatedOwner() {
+        MerchantSubaccountCreateRequest create = new MerchantSubaccountCreateRequest();
+        MerchantSubaccountUpdateRequest update = new MerchantSubaccountUpdateRequest();
+        MerchantSubaccountPasswordRequest password = new MerchantSubaccountPasswordRequest();
+        MerchantSubaccountStatusRequest status = new MerchantSubaccountStatusRequest();
+
+        controller.create("token", create);
+        controller.update("token", 20L, update);
+        controller.updatePassword("token", 20L, password);
+        controller.updateStatus("token", 20L, status);
+
+        verify(applicationService).create(10L, create);
+        verify(applicationService).update(10L, 20L, update);
+        verify(applicationService).updatePassword(10L, 20L, password);
+        verify(applicationService).updateOwnerStatus(10L, 20L, status);
+    }
+}

+ 2 - 0
ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml

@@ -73,6 +73,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="phone != null  and phone != ''"> and phone = #{phone}</if>
             <if test="nickName != null  and nickName != ''"> and BINARY nick_name like concat('%', #{nickName}, '%')</if>
             <if test="userType != null  and userType != ''"> and user_type = #{userType}</if>
+            <if test="merchantOwnerId != null"> and merchant_owner_id = #{merchantOwnerId}</if>
+            <if test="subaccountStatus != null and subaccountStatus != ''"> and subaccount_status = #{subaccountStatus}</if>
             <if test="email != null  and email != ''"> and email = #{email}</if>
             <if test="status != null  and status != ''"> and status = #{status}</if>
             <if test="delFlag != null  and delFlag != ''"> and del_flag = #{delFlag}</if>