feat(密钥管理): 按数据权限分组隔离与筛选
- 后台密钥列表改为按 biz_shop_manage_group 分组:主管只看自己带的分组成员,超管看全量可按 group_id 筛选 - 行数据补「所属分组」名称(批量查询);列表接口带回分组筛选项 - 修复超管筛选不生效:前端筛选参数统一 snake_case(camel 被后端静默忽略) - 列表列/筛选项文案由「所属管理员」改为「分组」
This commit is contained in:
@@ -19,8 +19,8 @@ export interface AdminUserSecretModule {
|
|||||||
export interface AdminUserSecretRow {
|
export interface AdminUserSecretRow {
|
||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
createdById: number | null
|
/** 所属数据权限分组名(可能多个)。 */
|
||||||
createdByUsername: string
|
groups: string[]
|
||||||
similarAsin: AdminUserSecretModule
|
similarAsin: AdminUserSecretModule
|
||||||
appearancePatent: AdminUserSecretModule
|
appearancePatent: AdminUserSecretModule
|
||||||
proxy: AdminUserSecretModule
|
proxy: AdminUserSecretModule
|
||||||
@@ -34,26 +34,31 @@ export interface AdminUserSecretPage {
|
|||||||
total: number
|
total: number
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
|
/** 分组筛选项(超管=全部;主管=自己带的分组)。 */
|
||||||
|
groupOptions?: GroupOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 管理员下拉项(超管筛选用)。 */
|
export interface GroupOption {
|
||||||
export interface AdminOption {
|
|
||||||
id: number
|
id: number
|
||||||
username: string
|
groupName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSecretQuery {
|
export interface UserSecretQuery {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
checkStatus?: string
|
checkStatus?: string
|
||||||
/** 按创建人筛选(仅超管生效)。 */
|
/** 按数据权限分组筛选(仅超管生效);后端参数为 snake_case。 */
|
||||||
createdById?: number
|
groupId?: number
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页查询用户密钥(一行一用户):GET /api/admin/user-secrets */
|
/** 分页查询用户密钥(一行一用户):GET /api/admin/user-secrets(筛选参数必须 snake_case,camel 会被后端静默忽略)。 */
|
||||||
export async function fetchUserSecretList(params: UserSecretQuery): Promise<AdminUserSecretPage> {
|
export async function fetchUserSecretList(params: UserSecretQuery): Promise<AdminUserSecretPage> {
|
||||||
const { data } = await http.get('/api/admin/user-secrets', { params })
|
const query: Record<string, string | number> = { page: params.page, pageSize: params.pageSize }
|
||||||
|
if (params.keyword) query.keyword = params.keyword
|
||||||
|
if (params.checkStatus) query.checkStatus = params.checkStatus
|
||||||
|
if (params.groupId) query.group_id = params.groupId
|
||||||
|
const { data } = await http.get('/api/admin/user-secrets', { params: query })
|
||||||
return unwrap<AdminUserSecretPage>(data)
|
return unwrap<AdminUserSecretPage>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,15 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { formatDateTime } from '@/utils/datetime'
|
import { formatDateTime } from '@/utils/datetime'
|
||||||
import OldPagination from '@/components/OldPagination.vue'
|
import OldPagination from '@/components/OldPagination.vue'
|
||||||
import { useAdminSessionStore } from '@/stores/admin-session'
|
|
||||||
import { fetchUserList } from '@/api/users'
|
|
||||||
import {
|
import {
|
||||||
checkUserSecret,
|
checkUserSecret,
|
||||||
deleteUserSecret,
|
deleteUserSecret,
|
||||||
fetchUserSecretList,
|
fetchUserSecretList,
|
||||||
type AdminUserSecretModule,
|
type AdminUserSecretModule,
|
||||||
type AdminUserSecretRow,
|
type AdminUserSecretRow,
|
||||||
|
type GroupOption,
|
||||||
} from '@/api/user-secrets'
|
} from '@/api/user-secrets'
|
||||||
|
|
||||||
const session = useAdminSessionStore()
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = ref<AdminUserSecretRow[]>([])
|
const rows = ref<AdminUserSecretRow[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -21,9 +19,9 @@ const page = ref(1)
|
|||||||
const pageSize = ref(15)
|
const pageSize = ref(15)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
const createdByIdFilter = ref<number | null>(null)
|
const groupFilter = ref<number | null>(null)
|
||||||
/** 管理员下拉(仅超管可见/加载)。 */
|
/** 分组下拉(超管=全部;主管=自己带的分组,由列表接口带回)。 */
|
||||||
const adminOptions = ref<Array<{ id: number; username: string }>>([])
|
const groupOptions = ref<GroupOption[]>([])
|
||||||
/** 正在检测的行 userId,用于按钮 loading 态。 */
|
/** 正在检测的行 userId,用于按钮 loading 态。 */
|
||||||
const checkingId = ref<number | null>(null)
|
const checkingId = ref<number | null>(null)
|
||||||
|
|
||||||
@@ -91,28 +89,19 @@ function rowStatusTooltip(row: AdminUserSecretRow) {
|
|||||||
return parts.join(';') || '暂无检测记录'
|
return parts.join(';') || '暂无检测记录'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAdminOptions() {
|
|
||||||
if (!session.isSuperAdmin) return
|
|
||||||
try {
|
|
||||||
const result = await fetchUserList({ page: 1, pageSize: 999, role: 'admin' })
|
|
||||||
adminOptions.value = result.admins || []
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[user-secrets] 管理员下拉加载失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchUserSecretList({
|
const result = await fetchUserSecretList({
|
||||||
keyword: keyword.value.trim() || undefined,
|
keyword: keyword.value.trim() || undefined,
|
||||||
checkStatus: statusFilter.value || undefined,
|
checkStatus: statusFilter.value || undefined,
|
||||||
createdById: createdByIdFilter.value || undefined,
|
groupId: groupFilter.value || undefined,
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
})
|
})
|
||||||
rows.value = result?.items || []
|
rows.value = result?.items || []
|
||||||
total.value = Number(result?.total || 0)
|
total.value = Number(result?.total || 0)
|
||||||
|
groupOptions.value = result?.groupOptions || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '加载失败')
|
ElMessage.error(error instanceof Error ? error.message : '加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -128,7 +117,7 @@ function search() {
|
|||||||
function reset() {
|
function reset() {
|
||||||
keyword.value = ''
|
keyword.value = ''
|
||||||
statusFilter.value = ''
|
statusFilter.value = ''
|
||||||
createdByIdFilter.value = null
|
groupFilter.value = null
|
||||||
page.value = 1
|
page.value = 1
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
@@ -183,10 +172,7 @@ function changeSize(size: number) {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(load)
|
||||||
loadAdminOptions()
|
|
||||||
load()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -207,11 +193,11 @@ onMounted(() => {
|
|||||||
<option v-for="option in STATUS_OPTIONS" :key="option.value" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in STATUS_OPTIONS" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" v-if="session.isSuperAdmin" style="min-width: 170px">
|
<div class="form-group" v-if="groupOptions.length" style="min-width: 170px">
|
||||||
<label>所属管理员</label>
|
<label>分组</label>
|
||||||
<select v-model="createdByIdFilter">
|
<select v-model="groupFilter">
|
||||||
<option :value="null">全部管理员</option>
|
<option :value="null">全部分组</option>
|
||||||
<option v-for="admin in adminOptions" :key="admin.id" :value="admin.id">{{ admin.username }}</option>
|
<option v-for="group in groupOptions" :key="group.id" :value="group.id">{{ group.groupName }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -229,7 +215,7 @@ onMounted(() => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 58px">序号</th>
|
<th style="width: 58px">序号</th>
|
||||||
<th style="width: 260px">用户</th>
|
<th style="width: 260px">用户</th>
|
||||||
<th style="width: 130px">所属管理员</th>
|
<th style="width: 130px">分组</th>
|
||||||
<th style="width: 230px">货源查询密钥</th>
|
<th style="width: 230px">货源查询密钥</th>
|
||||||
<th style="width: 230px">外观专利密钥</th>
|
<th style="width: 230px">外观专利密钥</th>
|
||||||
<th style="width: 230px">代理设置</th>
|
<th style="width: 230px">代理设置</th>
|
||||||
@@ -245,7 +231,7 @@ onMounted(() => {
|
|||||||
<span class="user-name">{{ row.username || '—' }}</span>
|
<span class="user-name">{{ row.username || '—' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="creator-name">{{ row.createdByUsername || (row.createdById ? `UID ${row.createdById}` : '—') }}</span>
|
<span class="creator-name">{{ row.groups?.length ? row.groups.join('、') : '—' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="module-cell">
|
<div class="module-cell">
|
||||||
@@ -296,7 +282,7 @@ onMounted(() => {
|
|||||||
<td colspan="8" class="empty-tip">加载中...</td>
|
<td colspan="8" class="empty-tip">加载中...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
<td colspan="8" class="empty-tip">{{ keyword || statusFilter || createdByIdFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
|
<td colspan="8" class="empty-tip">{{ keyword || statusFilter || groupFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+33
@@ -102,4 +102,37 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
|
|||||||
</script>
|
</script>
|
||||||
""")
|
""")
|
||||||
List<Long> selectUserIdsByGroupIds(@Param("groupIds") List<Long> groupIds);
|
List<Long> selectUserIdsByGroupIds(@Param("groupIds") List<Long> groupIds);
|
||||||
|
|
||||||
|
/** 批量查用户所属分组名(密钥管理列表「分组」列展示用)。 */
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT gm.user_id AS userId, g.group_name AS groupName
|
||||||
|
FROM biz_shop_manage_group_member gm
|
||||||
|
INNER JOIN biz_shop_manage_group g ON g.id = gm.group_id
|
||||||
|
WHERE gm.user_id IN
|
||||||
|
<foreach collection='userIds' item='userId' open='(' separator=',' close=')'>
|
||||||
|
#{userId}
|
||||||
|
</foreach>
|
||||||
|
ORDER BY g.id ASC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef> selectGroupNamesByUserIds(
|
||||||
|
@Param("userIds") List<Long> userIds);
|
||||||
|
|
||||||
|
/** 某人作为组长(创建人)的分组(密钥管理等按组隔离场景用)。 */
|
||||||
|
@Select("""
|
||||||
|
SELECT g.id AS id, g.group_name AS groupName
|
||||||
|
FROM biz_shop_manage_group g
|
||||||
|
WHERE g.created_by_id = #{operatorId} OR g.user_id = #{operatorId}
|
||||||
|
ORDER BY g.id ASC
|
||||||
|
""")
|
||||||
|
List<ShopManageGroupEntity> selectLedGroups(@Param("operatorId") Long operatorId);
|
||||||
|
|
||||||
|
/** 全部分组(超管筛选项用)。 */
|
||||||
|
@Select("""
|
||||||
|
SELECT g.id AS id, g.group_name AS groupName
|
||||||
|
FROM biz_shop_manage_group g
|
||||||
|
ORDER BY g.id ASC
|
||||||
|
""")
|
||||||
|
List<ShopManageGroupEntity> selectAllGroups();
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** 用户→分组名映射行(密钥管理列表展示用户所属分组用)。 */
|
||||||
|
@Data
|
||||||
|
public class UserGroupRef {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private String groupName;
|
||||||
|
}
|
||||||
+3
-3
@@ -39,19 +39,19 @@ public class AdminUserApiSecretController {
|
|||||||
private final AdminAuthSupport adminAuthSupport;
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询用户密钥", description = "一行一用户;keyword 匹配用户名,checkStatus 按行级状态筛选。主管只能看自己名下子账户,超管看全量并可按创建人筛选。")
|
@Operation(summary = "分页查询用户密钥", description = "一行一用户;keyword 匹配用户名,checkStatus 按行级状态筛选。主管只能看自己带的分组成员,超管看全量并可按分组筛选。")
|
||||||
public ApiResponse<AdminUserSecretPageVo> page(
|
public ApiResponse<AdminUserSecretPageVo> page(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "关键字:用户名") @RequestParam(required = false) String keyword,
|
@Parameter(description = "关键字:用户名") @RequestParam(required = false) String keyword,
|
||||||
@Parameter(description = "行级状态筛选:passed/failed/incomplete/error/unknown") @RequestParam(required = false) String checkStatus,
|
@Parameter(description = "行级状态筛选:passed/failed/incomplete/error/unknown") @RequestParam(required = false) String checkStatus,
|
||||||
@Parameter(description = "按创建人筛选(仅超管生效)") @RequestParam(name = "created_by_id", required = false) Long createdById,
|
@Parameter(description = "按数据权限分组筛选(仅超管生效)") @RequestParam(name = "group_id", required = false) Long groupId,
|
||||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
|
||||||
AdminUserEntity operator = adminAuthSupport.requireAdmin(request);
|
AdminUserEntity operator = adminAuthSupport.requireAdmin(request);
|
||||||
AdminUserSecretQuery query = new AdminUserSecretQuery();
|
AdminUserSecretQuery query = new AdminUserSecretQuery();
|
||||||
query.setKeyword(keyword);
|
query.setKeyword(keyword);
|
||||||
query.setCheckStatus(checkStatus);
|
query.setCheckStatus(checkStatus);
|
||||||
query.setCreatedById(createdById);
|
query.setGroupId(groupId);
|
||||||
query.setPage(page);
|
query.setPage(page);
|
||||||
query.setPageSize(pageSize);
|
query.setPageSize(pageSize);
|
||||||
return ApiResponse.success(userApiSecretService.adminPage(operator, query));
|
return ApiResponse.success(userApiSecretService.adminPage(operator, query));
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ public class AdminUserSecretQuery {
|
|||||||
@Schema(description = "行级状态筛选:passed/failed/incomplete/error/unknown")
|
@Schema(description = "行级状态筛选:passed/failed/incomplete/error/unknown")
|
||||||
private String checkStatus;
|
private String checkStatus;
|
||||||
|
|
||||||
@Schema(description = "按创建人筛选(仅超管生效;主管强制为本组)")
|
@Schema(description = "按数据权限分组筛选(超管可选;主管强制为自己带的分组)")
|
||||||
private Long createdById;
|
private Long groupId;
|
||||||
|
|
||||||
@Schema(description = "页码,从 1 开始")
|
@Schema(description = "页码,从 1 开始")
|
||||||
private Long page = 1L;
|
private Long page = 1L;
|
||||||
|
|||||||
+7
@@ -20,4 +20,11 @@ public class AdminUserSecretPageVo {
|
|||||||
|
|
||||||
@Schema(description = "每页数量")
|
@Schema(description = "每页数量")
|
||||||
private Long pageSize;
|
private Long pageSize;
|
||||||
|
|
||||||
|
@Schema(description = "分组筛选项(超管=全部分组,主管=自己带的分组)")
|
||||||
|
private List<GroupOptionVo> groupOptions;
|
||||||
|
|
||||||
|
@Schema(description = "分组筛选项")
|
||||||
|
public record GroupOptionVo(Long id, String groupName) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "后台密钥管理-单用户一行(三个字段列 + 行级状态)")
|
@Schema(description = "后台密钥管理-单用户一行(三个字段列 + 行级状态)")
|
||||||
@@ -15,11 +16,8 @@ public class AdminUserSecretRowVo {
|
|||||||
@Schema(description = "用户名")
|
@Schema(description = "用户名")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "所属管理员(创建人)ID")
|
@Schema(description = "所属分组名(数据权限分组,可能多个)")
|
||||||
private Long createdById;
|
private List<String> groups;
|
||||||
|
|
||||||
@Schema(description = "所属管理员用户名")
|
|
||||||
private String createdByUsername;
|
|
||||||
|
|
||||||
@Schema(description = "货源查询密钥")
|
@Schema(description = "货源查询密钥")
|
||||||
private AdminUserSecretModuleVo similarAsin;
|
private AdminUserSecretModuleVo similarAsin;
|
||||||
|
|||||||
+102
-31
@@ -6,6 +6,9 @@ import com.nanri.aiimage.common.security.ShopCredentialCryptoService;
|
|||||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||||
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;import com.nanri.aiimage.modules.usersecret.mapper.UserApiSecretMapper;
|
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;import com.nanri.aiimage.modules.usersecret.mapper.UserApiSecretMapper;
|
||||||
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretQuery;
|
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretQuery;
|
||||||
import com.nanri.aiimage.modules.usersecret.model.dto.UserApiSecretMigrateRequest;
|
import com.nanri.aiimage.modules.usersecret.model.dto.UserApiSecretMigrateRequest;
|
||||||
@@ -69,6 +72,7 @@ public class UserApiSecretService {
|
|||||||
private final JikipProxyClient jikipProxyClient;
|
private final JikipProxyClient jikipProxyClient;
|
||||||
private final AdminUserMapper adminUserMapper;
|
private final AdminUserMapper adminUserMapper;
|
||||||
private final AdminAuthSupport adminAuthSupport;
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
private final ShopManageGroupMapper adminGroupMapper;
|
||||||
|
|
||||||
/** 当前用户密钥包:仅必填模块(代理为选配,不下发)+ 服务端下发的必填清单 + 完整性判定。 */
|
/** 当前用户密钥包:仅必填模块(代理为选配,不下发)+ 服务端下发的必填清单 + 完整性判定。 */
|
||||||
public UserApiSecretBundleVo bundle(Long userId) {
|
public UserApiSecretBundleVo bundle(Long userId) {
|
||||||
@@ -200,43 +204,50 @@ public class UserApiSecretService {
|
|||||||
* 先按关键字圈定用户,再全量聚合、行级状态筛选、按最近更新时间倒序后内存分页
|
* 先按关键字圈定用户,再全量聚合、行级状态筛选、按最近更新时间倒序后内存分页
|
||||||
* (当前规模为用户数×3,内存聚合成本可控;量级上来后可改为 GROUP BY 下推)。
|
* (当前规模为用户数×3,内存聚合成本可控;量级上来后可改为 GROUP BY 下推)。
|
||||||
*/
|
*/
|
||||||
/** 后台分页查询:主管(admin)只能看自己名下子账户(created_by_id=自己),超管看全量可按创建人筛选。 */
|
/** 后台分页查询:主管(admin)只看自己带的「数据权限分组」成员;超管看全量、可按分组筛选。 */
|
||||||
public AdminUserSecretPageVo adminPage(AdminUserEntity operator, AdminUserSecretQuery query) {
|
public AdminUserSecretPageVo adminPage(AdminUserEntity operator, AdminUserSecretQuery query) {
|
||||||
AdminUserSecretQuery safeQuery = query == null ? new AdminUserSecretQuery() : query;
|
AdminUserSecretQuery safeQuery = query == null ? new AdminUserSecretQuery() : query;
|
||||||
long page = safeQuery.getPage() == null || safeQuery.getPage() < 1 ? 1L : safeQuery.getPage();
|
long page = safeQuery.getPage() == null || safeQuery.getPage() < 1 ? 1L : safeQuery.getPage();
|
||||||
long pageSize = safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1
|
long pageSize = safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1
|
||||||
? 15L : Math.min(safeQuery.getPageSize(), 100L);
|
? 15L : Math.min(safeQuery.getPageSize(), 100L);
|
||||||
|
|
||||||
// 组隔离:主管强制锁定本组;超管可按 created_by_id 筛选(0 或空 = 全部)。
|
|
||||||
boolean superAdmin = "super_admin".equals(adminAuthSupport.currentRole(operator));
|
boolean superAdmin = "super_admin".equals(adminAuthSupport.currentRole(operator));
|
||||||
Long scopedCreatedById;
|
Long requestedGroupId = safeQuery.getGroupId();
|
||||||
|
List<Long> ledGroupIds = superAdmin ? List.of() : listLedGroupIds(operator.getId());
|
||||||
|
// 组隔离:主管强制锁定自己带的分组;超管按 group_id 筛选(空 = 全部)。
|
||||||
|
List<Long> scopedGroupIds;
|
||||||
if (superAdmin) {
|
if (superAdmin) {
|
||||||
Long filter = safeQuery.getCreatedById();
|
scopedGroupIds = requestedGroupId != null && requestedGroupId > 0 ? List.of(requestedGroupId) : null;
|
||||||
scopedCreatedById = filter != null && filter > 0 ? filter : null;
|
|
||||||
} else {
|
} else {
|
||||||
scopedCreatedById = operator.getId();
|
if (ledGroupIds.isEmpty()) {
|
||||||
|
log.info("[user-secret] 后台密钥列表:主管未带任何分组,返回空 operatorId={}", operator.getId());
|
||||||
|
return emptyPageWithGroups(page, pageSize, List.of());
|
||||||
|
}
|
||||||
|
scopedGroupIds = ledGroupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<UserApiSecretEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<UserApiSecretEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
String keyword = normalize(safeQuery.getKeyword());
|
String keyword = normalize(safeQuery.getKeyword());
|
||||||
List<Long> allowedUserIds = null;
|
if (!keyword.isEmpty() || scopedGroupIds != null) {
|
||||||
// 用户存在性/归属先按 users 表圈定:keyword 匹配 + 组隔离前置过滤(无密钥记录的用户本来就不在聚合表里)。
|
// 分组圈定成员用户(含组长本人);再叠加用户名关键字。
|
||||||
LambdaQueryWrapper<AdminUserEntity> userWrapper = new LambdaQueryWrapper<>();
|
List<Long> allowedUserIds = scopedGroupIds == null
|
||||||
if (!keyword.isEmpty()) {
|
? new ArrayList<>()
|
||||||
userWrapper.like(AdminUserEntity::getUsername, keyword);
|
: new ArrayList<>(adminGroupMapper.selectUserIdsByGroupIds(scopedGroupIds));
|
||||||
}
|
if (!keyword.isEmpty()) {
|
||||||
if (scopedCreatedById != null) {
|
List<Long> matched = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
|
||||||
userWrapper.eq(AdminUserEntity::getCreatedById, scopedCreatedById);
|
.like(AdminUserEntity::getUsername, keyword)
|
||||||
}
|
.last("limit 200"))
|
||||||
if (!keyword.isEmpty() || scopedCreatedById != null) {
|
.stream().map(AdminUserEntity::getId).filter(id -> id != null).toList();
|
||||||
allowedUserIds = adminUserMapper.selectList(userWrapper).stream()
|
if (scopedGroupIds == null) {
|
||||||
.map(AdminUserEntity::getId)
|
allowedUserIds = new ArrayList<>(matched);
|
||||||
.filter(id -> id != null)
|
} else {
|
||||||
.toList();
|
allowedUserIds.retainAll(matched);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (allowedUserIds.isEmpty()) {
|
if (allowedUserIds.isEmpty()) {
|
||||||
log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} createdById={} operatorId={} role={}",
|
log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} groupIds={} operatorId={} role={}",
|
||||||
keyword, scopedCreatedById, operator.getId(), superAdmin ? "super_admin" : "admin");
|
keyword, scopedGroupIds, operator.getId(), superAdmin ? "super_admin" : "admin");
|
||||||
return emptyPage(page, pageSize);
|
return emptyPageWithGroups(page, pageSize, groupOptions(operator, superAdmin, ledGroupIds));
|
||||||
}
|
}
|
||||||
wrapper.in(UserApiSecretEntity::getUserId, allowedUserIds);
|
wrapper.in(UserApiSecretEntity::getUserId, allowedUserIds);
|
||||||
}
|
}
|
||||||
@@ -265,13 +276,77 @@ public class UserApiSecretService {
|
|||||||
long total = all.size();
|
long total = all.size();
|
||||||
int from = (int) Math.min((page - 1) * pageSize, total);
|
int from = (int) Math.min((page - 1) * pageSize, total);
|
||||||
int to = (int) Math.min(from + pageSize, total);
|
int to = (int) Math.min(from + pageSize, total);
|
||||||
|
List<AdminUserSecretRowVo> pageItems = new ArrayList<>(all.subList(from, to));
|
||||||
|
fillRowGroups(pageItems);
|
||||||
|
|
||||||
AdminUserSecretPageVo vo = new AdminUserSecretPageVo();
|
AdminUserSecretPageVo vo = new AdminUserSecretPageVo();
|
||||||
vo.setItems(new ArrayList<>(all.subList(from, to)));
|
vo.setItems(pageItems);
|
||||||
vo.setTotal(total);
|
vo.setTotal(total);
|
||||||
vo.setPage(page);
|
vo.setPage(page);
|
||||||
vo.setPageSize(pageSize);
|
vo.setPageSize(pageSize);
|
||||||
log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} createdById={} role={} 聚合用户数={} 本页返回={}",
|
vo.setGroupOptions(groupOptions(operator, superAdmin, ledGroupIds));
|
||||||
keyword, statusFilter, scopedCreatedById, superAdmin ? "super_admin" : "admin", total, vo.getItems().size());
|
log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} groupIds={} role={} 聚合用户数={} 本页返回={}",
|
||||||
|
keyword, statusFilter, scopedGroupIds, superAdmin ? "super_admin" : "admin", total, pageItems.size());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主管带的分组 ID(created_by_id / user_id = 自己)。 */
|
||||||
|
private List<Long> listLedGroupIds(Long operatorId) {
|
||||||
|
if (operatorId == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return adminGroupMapper.selectLedGroups(operatorId).stream()
|
||||||
|
.map(ShopManageGroupEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分组筛选项:超管=全部;主管=自己带的分组(没有则不展示下拉)。 */
|
||||||
|
private List<AdminUserSecretPageVo.GroupOptionVo> groupOptions(
|
||||||
|
AdminUserEntity operator, boolean superAdmin, List<Long> ledGroupIds) {
|
||||||
|
List<ShopManageGroupEntity> groups;
|
||||||
|
if (superAdmin) {
|
||||||
|
groups = adminGroupMapper.selectAllGroups();
|
||||||
|
} else if (ledGroupIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
} else {
|
||||||
|
groups = adminGroupMapper.selectLedGroups(operator.getId());
|
||||||
|
}
|
||||||
|
return groups.stream()
|
||||||
|
.filter(group -> group.getId() != null)
|
||||||
|
.map(group -> new AdminUserSecretPageVo.GroupOptionVo(group.getId(), group.getGroupName()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本页行补「所属分组」:一次批量查询,避免逐行查库。 */
|
||||||
|
private void fillRowGroups(List<AdminUserSecretRowVo> pageItems) {
|
||||||
|
List<Long> userIds = pageItems.stream()
|
||||||
|
.map(AdminUserSecretRowVo::getUserId)
|
||||||
|
.filter(id -> id != null)
|
||||||
|
.toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<Long, List<String>> byUser = new LinkedHashMap<>();
|
||||||
|
for (UserGroupRef ref : adminGroupMapper.selectGroupNamesByUserIds(userIds)) {
|
||||||
|
if (ref.getUserId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = normalize(ref.getGroupName());
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byUser.computeIfAbsent(ref.getUserId(), key -> new ArrayList<>()).add(name);
|
||||||
|
}
|
||||||
|
for (AdminUserSecretRowVo item : pageItems) {
|
||||||
|
item.setGroups(byUser.getOrDefault(item.getUserId(), List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminUserSecretPageVo emptyPageWithGroups(
|
||||||
|
long page, long pageSize, List<AdminUserSecretPageVo.GroupOptionVo> groupOptions) {
|
||||||
|
AdminUserSecretPageVo vo = emptyPage(page, pageSize);
|
||||||
|
vo.setGroupOptions(groupOptions);
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,11 +487,7 @@ public class UserApiSecretService {
|
|||||||
vo.setUpdatedAt(latestUpdatedAt(modules));
|
vo.setUpdatedAt(latestUpdatedAt(modules));
|
||||||
AdminUserEntity user = adminUserMapper.selectById(userId);
|
AdminUserEntity user = adminUserMapper.selectById(userId);
|
||||||
vo.setUsername(user == null ? "" : user.getUsername());
|
vo.setUsername(user == null ? "" : user.getUsername());
|
||||||
if (user != null && user.getCreatedById() != null) {
|
vo.setGroups(List.of());
|
||||||
vo.setCreatedById(user.getCreatedById());
|
|
||||||
AdminUserEntity creator = adminUserMapper.selectById(user.getCreatedById());
|
|
||||||
vo.setCreatedByUsername(creator == null ? "" : creator.getUsername());
|
|
||||||
}
|
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-16
@@ -4,6 +4,8 @@ import com.nanri.aiimage.common.security.ShopCredentialCryptoService;
|
|||||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||||
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;
|
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;
|
||||||
import com.nanri.aiimage.modules.usersecret.mapper.UserApiSecretMapper;
|
import com.nanri.aiimage.modules.usersecret.mapper.UserApiSecretMapper;
|
||||||
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretQuery;
|
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretQuery;
|
||||||
@@ -32,6 +34,8 @@ class UserApiSecretServiceTest {
|
|||||||
private final JikipProxyClient jikipProxyClient = mock(JikipProxyClient.class);
|
private final JikipProxyClient jikipProxyClient = mock(JikipProxyClient.class);
|
||||||
private final AdminUserMapper adminUserMapper = mock(AdminUserMapper.class);
|
private final AdminUserMapper adminUserMapper = mock(AdminUserMapper.class);
|
||||||
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
|
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
|
||||||
|
private final com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper adminGroupMapper =
|
||||||
|
mock(com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper.class);
|
||||||
|
|
||||||
private UserApiSecretService newService() {
|
private UserApiSecretService newService() {
|
||||||
when(crypto.encrypt(anyString())).thenAnswer(inv -> "enc:" + inv.getArgument(0, String.class));
|
when(crypto.encrypt(anyString())).thenAnswer(inv -> "enc:" + inv.getArgument(0, String.class));
|
||||||
@@ -41,7 +45,8 @@ class UserApiSecretServiceTest {
|
|||||||
});
|
});
|
||||||
// 默认按主管(admin)判定;超管用例里单独改打桩。
|
// 默认按主管(admin)判定;超管用例里单独改打桩。
|
||||||
when(adminAuthSupport.currentRole(any())).thenReturn("admin");
|
when(adminAuthSupport.currentRole(any())).thenReturn("admin");
|
||||||
return new UserApiSecretService(mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport);
|
return new UserApiSecretService(
|
||||||
|
mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport, adminGroupMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -210,8 +215,6 @@ class UserApiSecretServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void adminPageAggregatesOneRowPerUserWithProxyMasked() {
|
void adminPageAggregatesOneRowPerUserWithProxyMasked() {
|
||||||
UserApiSecretService service = newService();
|
UserApiSecretService service = newService();
|
||||||
// 非限定查询:无 keyword 无组过滤时不触发 users 表圈定查询。
|
|
||||||
when(adminUserMapper.selectList(any())).thenReturn(List.of());
|
|
||||||
when(mapper.selectList(any())).thenReturn(List.of(
|
when(mapper.selectList(any())).thenReturn(List.of(
|
||||||
row(1L, "similar-asin", "enc:sk-sa-1234", "passed"),
|
row(1L, "similar-asin", "enc:sk-sa-1234", "passed"),
|
||||||
row(1L, "appearance-patent", "enc:sk-ap-5678", "passed"),
|
row(1L, "appearance-patent", "enc:sk-ap-5678", "passed"),
|
||||||
@@ -220,11 +223,11 @@ class UserApiSecretServiceTest {
|
|||||||
user.setId(1L);
|
user.setId(1L);
|
||||||
user.setUsername("张三");
|
user.setUsername("张三");
|
||||||
when(adminUserMapper.selectById(1L)).thenReturn(user);
|
when(adminUserMapper.selectById(1L)).thenReturn(user);
|
||||||
AdminUserEntity creator = new AdminUserEntity();
|
// 分组列:用户 1 属于「一组」
|
||||||
creator.setId(99L);
|
UserGroupRef ref = new UserGroupRef();
|
||||||
creator.setUsername("主管甲");
|
ref.setUserId(1L);
|
||||||
when(adminUserMapper.selectById(99L)).thenReturn(creator);
|
ref.setGroupName("一组");
|
||||||
user.setCreatedById(99L);
|
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of(ref));
|
||||||
|
|
||||||
AdminUserEntity operator = new AdminUserEntity();
|
AdminUserEntity operator = new AdminUserEntity();
|
||||||
operator.setId(88L);
|
operator.setId(88L);
|
||||||
@@ -234,8 +237,7 @@ class UserApiSecretServiceTest {
|
|||||||
assertThat(page.getItems()).hasSize(1);
|
assertThat(page.getItems()).hasSize(1);
|
||||||
var rowVo = page.getItems().get(0);
|
var rowVo = page.getItems().get(0);
|
||||||
assertThat(rowVo.getUsername()).isEqualTo("张三");
|
assertThat(rowVo.getUsername()).isEqualTo("张三");
|
||||||
assertThat(rowVo.getCreatedById()).isEqualTo(99L);
|
assertThat(rowVo.getGroups()).containsExactly("一组");
|
||||||
assertThat(rowVo.getCreatedByUsername()).isEqualTo("主管甲");
|
|
||||||
assertThat(rowVo.getStatus()).isEqualTo("failed");
|
assertThat(rowVo.getStatus()).isEqualTo("failed");
|
||||||
assertThat(rowVo.getSimilarAsin().getMasked()).isEqualTo("sk-s****1234");
|
assertThat(rowVo.getSimilarAsin().getMasked()).isEqualTo("sk-s****1234");
|
||||||
assertThat(rowVo.getProxy().getExists()).isTrue();
|
assertThat(rowVo.getProxy().getExists()).isTrue();
|
||||||
@@ -243,20 +245,61 @@ class UserApiSecretServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void adminPageLocksAdminToOwnGroupUsers() {
|
void adminPageLocksAdminToOwnLedGroups() {
|
||||||
UserApiSecretService service = newService();
|
UserApiSecretService service = newService();
|
||||||
// 主管无论传什么 createdById,都强制锁定为本组(created_by_id=88L)。
|
|
||||||
AdminUserEntity operator = new AdminUserEntity();
|
AdminUserEntity operator = new AdminUserEntity();
|
||||||
operator.setId(88L);
|
operator.setId(88L);
|
||||||
when(adminUserMapper.selectList(any())).thenReturn(List.of());
|
// 主管未带任何分组 → 直接空页,不查密钥表。
|
||||||
|
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of());
|
||||||
|
|
||||||
service.adminPage(operator, new AdminUserSecretQuery());
|
var page = service.adminPage(operator, new AdminUserSecretQuery());
|
||||||
|
|
||||||
// 主管模式:先圈定 users 表(组过滤),密钥表因无匹配行不再查询。
|
assertThat(page.getItems()).isEmpty();
|
||||||
verify(adminUserMapper).selectList(any());
|
|
||||||
verify(mapper, never()).selectList(any());
|
verify(mapper, never()).selectList(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminPageScopesToLedGroupMembers() {
|
||||||
|
UserApiSecretService service = newService();
|
||||||
|
AdminUserEntity operator = new AdminUserEntity();
|
||||||
|
operator.setId(88L);
|
||||||
|
ShopManageGroupEntity group = new ShopManageGroupEntity();
|
||||||
|
group.setId(5L);
|
||||||
|
group.setGroupName("一组");
|
||||||
|
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of(group));
|
||||||
|
when(adminGroupMapper.selectUserIdsByGroupIds(any())).thenReturn(List.of(1L));
|
||||||
|
when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed")));
|
||||||
|
AdminUserEntity user = new AdminUserEntity();
|
||||||
|
user.setId(1L);
|
||||||
|
user.setUsername("张三");
|
||||||
|
when(adminUserMapper.selectById(1L)).thenReturn(user);
|
||||||
|
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
var page = service.adminPage(operator, new AdminUserSecretQuery());
|
||||||
|
|
||||||
|
assertThat(page.getItems()).hasSize(1);
|
||||||
|
assertThat(page.getGroupOptions()).extracting(com.nanri.aiimage.modules.usersecret.model.vo.AdminUserSecretPageVo.GroupOptionVo::groupName)
|
||||||
|
.containsExactly("一组");
|
||||||
|
verify(adminGroupMapper).selectUserIdsByGroupIds(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminPageFiltersSuperAdminByGroupId() {
|
||||||
|
UserApiSecretService service = newService();
|
||||||
|
AdminUserEntity operator = new AdminUserEntity();
|
||||||
|
operator.setId(88L);
|
||||||
|
when(adminAuthSupport.currentRole(operator)).thenReturn("super_admin");
|
||||||
|
when(adminGroupMapper.selectUserIdsByGroupIds(List.of(5L))).thenReturn(List.of());
|
||||||
|
when(adminGroupMapper.selectAllGroups()).thenReturn(List.of());
|
||||||
|
|
||||||
|
AdminUserSecretQuery query = new AdminUserSecretQuery();
|
||||||
|
query.setGroupId(5L);
|
||||||
|
var page = service.adminPage(operator, query);
|
||||||
|
|
||||||
|
assertThat(page.getItems()).isEmpty();
|
||||||
|
verify(adminGroupMapper).selectUserIdsByGroupIds(List.of(5L));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void adminClearByUserDeletesAllRowsOfUser() {
|
void adminClearByUserDeletesAllRowsOfUser() {
|
||||||
UserApiSecretService service = newService();
|
UserApiSecretService service = newService();
|
||||||
|
|||||||
Reference in New Issue
Block a user