Aias00 commented on code in PR #6145:
URL: https://github.com/apache/shenyu/pull/6145#discussion_r2371280892


##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/AbstractDataChangedListener.java:
##########
@@ -190,6 +197,16 @@ public void onPluginChanged(final List<PluginData> 
changed, final DataEventTypeE
      * @param eventType the event type
      */
     protected void afterPluginChanged(final List<PluginData> changed, final 
DataEventTypeEnum eventType, final String namespaceId) {
+        // When AI Proxy plugin is synchronized, also refresh proxy apikey 
cache so it is included in the same sync cycle
+        if (CollectionUtils.isEmpty(changed)) {
+            return;
+        }
+        boolean containsAiProxy = changed.stream()
+                .filter(Objects::nonNull)
+                .anyMatch(p -> 
org.apache.commons.lang3.StringUtils.equalsIgnoreCase(p.getName(), 
org.apache.shenyu.common.enums.PluginEnum.AI_PROXY.getName()));

Review Comment:
   just import the class, not like this: 
"org.apache.commons.lang3.StringUtils.equalsIgnoreCase"



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/AiProxySelectorResolverInvalidator.java:
##########
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.listener;
+
+import org.apache.shenyu.admin.service.support.AiProxyRealKeyResolver;
+import org.apache.shenyu.common.dto.ProxySelectorData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Invalidate AiProxy real-key resolver cache only when aiProxy selector 
changes.
+ */
+@Component
+public class AiProxySelectorResolverInvalidator implements DataChangedListener 
{
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiProxySelectorResolverInvalidator.class);
+
+    private final AiProxyRealKeyResolver resolver;
+
+    private final org.apache.shenyu.admin.service.AiProxyConnectionService 
aiProxyConnectionService;
+
+    public AiProxySelectorResolverInvalidator(final AiProxyRealKeyResolver 
resolver,
+                                              final 
org.apache.shenyu.admin.service.AiProxyConnectionService 
aiProxyConnectionService) {
+        this.resolver = resolver;
+        this.aiProxyConnectionService = aiProxyConnectionService;
+    }
+
+    @Override
+    public void onSelectorChanged(final List<SelectorData> changed, final 
DataEventTypeEnum eventType) {
+        if (Objects.isNull(changed) || changed.isEmpty()) {

Review Comment:
   CollectionUtils.isEmpty



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AiProxyApiKeyController.java:
##########
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.aspect.annotation.RestApi;
+import org.apache.shenyu.admin.model.dto.BatchCommonDTO;
+import org.apache.shenyu.admin.model.dto.ProxyApiKeyDTO;
+import org.apache.shenyu.admin.model.page.CommonPager;
+import org.apache.shenyu.admin.model.page.PageParameter;
+import org.apache.shenyu.admin.model.query.ProxyApiKeyQuery;
+import org.apache.shenyu.admin.model.result.ShenyuAdminResult;
+import org.apache.shenyu.admin.model.vo.ProxyApiKeyVO;
+import org.apache.shenyu.admin.service.AiProxyApiKeyService;
+import org.apache.shenyu.admin.utils.ShenyuResultMessage;
+import org.apache.shenyu.admin.service.PageService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+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.RequestParam;
+
+import org.apache.shenyu.admin.model.dto.BatchIdsDTO;
+import org.apache.shenyu.admin.mapper.SelectorMapper;
+import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.common.constant.AdminConstants;
+import java.util.ArrayList;
+import java.util.Objects;
+
+/** AiProxyApiKeyController. */
+@RestApi("/selector/{selectorId}/ai-proxy-apikey")
+public class AiProxyApiKeyController implements 
PagedController<ProxyApiKeyQuery, ProxyApiKeyVO> {
+
+    private final AiProxyApiKeyService aiProxyApiKeyService;
+
+    private final SelectorMapper selectorMapper;
+
+    public AiProxyApiKeyController(final AiProxyApiKeyService 
aiProxyApiKeyService, final SelectorMapper selectorMapper) {
+        this.aiProxyApiKeyService = aiProxyApiKeyService;
+        this.selectorMapper = selectorMapper;
+    }
+
+    /**
+     * Create or bind real->proxy api key mapping.
+     *
+     * @param selectorId selector id
+     * @param dto dto
+     * @return result
+     */
+    @PostMapping
+    @RequiresPermissions("system:aiProxyApiKey:add")
+    public ShenyuAdminResult create(@PathVariable("selectorId") final String 
selectorId,
+                                    @Valid @RequestBody final ProxyApiKeyDTO 
dto) {
+        // derive namespaceId from selector to avoid mismatch
+        final SelectorDO selector = selectorMapper.selectById(selectorId);
+        if (Objects.nonNull(selector)) {
+            dto.setNamespaceId(selector.getNamespaceId());
+        }
+        int rows = aiProxyApiKeyService.create(dto, selectorId);
+        if (rows > 0) {
+            final ProxyApiKeyVO vo = 
aiProxyApiKeyService.findById(dto.getId());
+            return 
ShenyuAdminResult.success(ShenyuResultMessage.CREATE_SUCCESS, vo);
+        }
+        return ShenyuAdminResult.error(ShenyuResultMessage.PARAMETER_ERROR);
+    }
+
+    /**
+     * List mappings by page.
+     *
+     * @param selectorId selector id
+     * @param namespaceId namespace id
+     * @param currentPage current page
+     * @param pageSize page size
+     * @param proxyApiKey optional filter
+     * @return list pager
+     */
+    @GetMapping
+    @RequiresPermissions("system:aiProxyApiKey:list")
+    public ShenyuAdminResult list(
+            @PathVariable("selectorId") final String selectorId,
+            @RequestParam(value = "namespaceId", required = false) final 
String namespaceId,
+            @RequestParam("currentPage") @NotNull final Integer currentPage,
+            @RequestParam("pageSize") @NotNull final Integer pageSize,
+            @RequestParam(value = "proxyApiKey", required = false) final 
String proxyApiKey) {
+        // derive namespaceId from selectorId to ensure filtering is bound to 
selector
+        final SelectorDO selector = selectorMapper.selectById(selectorId);
+        if (Objects.isNull(selector)) {
+            return ShenyuAdminResult.error(AdminConstants.ID_NOT_EXIST);
+        }
+        ProxyApiKeyQuery query = new ProxyApiKeyQuery();
+        // only filter by selectorId to ensure strict scoping
+        query.setSelectorId(selectorId);
+        query.setNamespaceId(selector.getNamespaceId());
+        query.setProxyApiKey(proxyApiKey);
+        query.setPageParameter(new PageParameter(currentPage, pageSize));
+        CommonPager<ProxyApiKeyVO> pager = 
aiProxyApiKeyService.listByPage(query);
+        return ShenyuAdminResult.success(ShenyuResultMessage.QUERY_SUCCESS, 
pager);
+    }
+
+    /**
+     * Update mapping description or enabled flag.
+     *
+     * @param selectorId selector id
+     * @param id id
+     * @param dto dto
+     * @return result
+     */
+    @PutMapping("/{id}")
+    @RequiresPermissions("system:aiProxyApiKey:edit")
+    public ShenyuAdminResult update(
+            @PathVariable("selectorId") final String selectorId,
+            @PathVariable("id") final String id, @RequestBody final 
ProxyApiKeyDTO dto) {
+        final ProxyApiKeyVO exist = aiProxyApiKeyService.findById(id);
+        if (Objects.isNull(exist)) {
+            return ShenyuAdminResult.error(AdminConstants.ID_NOT_EXIST);
+        }
+        if (!selectorId.equals(exist.getSelectorId())) {
+            return 
ShenyuAdminResult.error(AdminConstants.PROXY_SELECTOR_ID_IS_NOT_EXIST);
+        }
+        dto.setId(id);
+        int rows = aiProxyApiKeyService.update(dto);
+        return rows > 0
+                ? ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS)
+                : ShenyuAdminResult.error(ShenyuResultMessage.PARAMETER_ERROR);
+    }
+
+    /**
+     * Batch delete.
+     *
+     * @param selectorId selector id
+     * @param request ids container
+     * @return result
+     */
+    @PostMapping("/batchDelete")
+    @RequiresPermissions("system:aiProxyApiKey:delete")
+    public ShenyuAdminResult batchDelete(@PathVariable("selectorId") final 
String selectorId,
+                                         @Valid @RequestBody final BatchIdsDTO 
request) {
+        final java.util.List<String> allIds = request.getIds();
+        final java.util.List<String> validIds = new ArrayList<>();
+        if (Objects.nonNull(allIds)) {
+            for (String id : allIds) {
+                final ProxyApiKeyVO exist = aiProxyApiKeyService.findById(id);
+                if (Objects.nonNull(exist) && 
selectorId.equals(exist.getSelectorId())) {
+                    validIds.add(id);
+                }
+            }
+        }
+        if (validIds.isEmpty()) {
+            return 
ShenyuAdminResult.error(AdminConstants.PROXY_SELECTOR_ID_IS_NOT_EXIST);
+        }
+        int rows = aiProxyApiKeyService.delete(validIds);
+        return rows > 0
+                ? ShenyuAdminResult.success(ShenyuResultMessage.DELETE_SUCCESS)
+                : ShenyuAdminResult.error(ShenyuResultMessage.DELETE_FAIL);
+    }
+
+    /**
+     * Batch enable/disable.
+     *
+     * @param selectorId selector id
+     * @param batchCommonDTO ids and enabled
+     * @return result
+     */
+    @PostMapping("/batchEnabled")
+    @RequiresPermissions("system:aiProxyApiKey:disable")
+    public ShenyuAdminResult batchEnabled(@PathVariable("selectorId") final 
String selectorId,
+                                          @Valid @RequestBody final 
BatchCommonDTO batchCommonDTO) {
+        final java.util.List<String> allIds = batchCommonDTO.getIds();

Review Comment:
   not use "java.util.List", just import 



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/AiProxySelectorResolverInvalidator.java:
##########
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.listener;
+
+import org.apache.shenyu.admin.service.support.AiProxyRealKeyResolver;
+import org.apache.shenyu.common.dto.ProxySelectorData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Invalidate AiProxy real-key resolver cache only when aiProxy selector 
changes.
+ */
+@Component
+public class AiProxySelectorResolverInvalidator implements DataChangedListener 
{
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiProxySelectorResolverInvalidator.class);
+
+    private final AiProxyRealKeyResolver resolver;
+
+    private final org.apache.shenyu.admin.service.AiProxyConnectionService 
aiProxyConnectionService;
+
+    public AiProxySelectorResolverInvalidator(final AiProxyRealKeyResolver 
resolver,
+                                              final 
org.apache.shenyu.admin.service.AiProxyConnectionService 
aiProxyConnectionService) {
+        this.resolver = resolver;
+        this.aiProxyConnectionService = aiProxyConnectionService;
+    }
+
+    @Override
+    public void onSelectorChanged(final List<SelectorData> changed, final 
DataEventTypeEnum eventType) {
+        if (Objects.isNull(changed) || changed.isEmpty()) {
+            return;
+        }
+        for (SelectorData d : changed) {
+            if (Objects.isNull(d) || Objects.isNull(d.getId())) {
+                continue;
+            }
+            if (!PluginEnum.AI_PROXY.getName().equals(d.getPluginName())) {
+                continue;
+            }
+            resolver.invalidate(d.getId());
+            LOG.info("[AiProxyResolverInvalidator] invalidated selectorId={} 
due to {} (selector)", d.getId(), eventType);
+            try {
+                if 
(TransactionSynchronizationManager.isSynchronizationActive()) {
+                    
TransactionSynchronizationManager.registerSynchronization(new 
TransactionSynchronization() {
+                        @Override
+                        public void afterCommit() {
+                            
aiProxyConnectionService.refreshApiKeysBySelectorId(d.getId());
+                        }
+                    });
+                } else {
+                    
aiProxyConnectionService.refreshApiKeysBySelectorId(d.getId());
+                }
+            } catch (Exception ex) {
+                LOG.warn("[AiProxyResolverInvalidator] failed to refresh 
AI_PROXY_API_KEY for selectorId={}: {}", d.getId(), ex.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public void onProxySelectorChanged(final List<ProxySelectorData> changed, 
final DataEventTypeEnum eventType) {
+        if (Objects.isNull(changed) || changed.isEmpty()) {

Review Comment:
   change this



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AiProxyApiKeyServiceImpl.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.mapper.AiProxyApiKeyMapper;
+import org.apache.shenyu.admin.model.dto.ProxyApiKeyDTO;
+import org.apache.shenyu.admin.model.entity.ProxyApiKeyDO;
+import org.apache.shenyu.admin.model.page.CommonPager;
+import org.apache.shenyu.admin.model.page.PageParameter;
+import org.apache.shenyu.admin.model.query.ProxyApiKeyQuery;
+import org.apache.shenyu.admin.model.vo.ProxyApiKeyVO;
+import org.apache.shenyu.admin.service.AiProxyApiKeyService;
+import org.apache.shenyu.admin.transfer.ProxyApiKeyTransfer;
+import org.apache.shenyu.admin.service.support.AiProxyRealKeyResolver;
+import org.apache.shenyu.admin.utils.ShenyuResultMessage;
+import org.apache.shenyu.common.dto.ProxyApiKeyData;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.utils.SignUtils;
+import org.apache.shenyu.common.utils.UUIDUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.shenyu.common.constant.Constants;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.apache.shenyu.common.constant.AdminConstants;
+import org.apache.shenyu.common.exception.ShenyuException;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/** Implementation of AiProxyApiKeyService. */
+@Service
+public class AiProxyApiKeyServiceImpl implements AiProxyApiKeyService {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiProxyApiKeyServiceImpl.class);
+
+    private final AiProxyApiKeyMapper mapper;
+
+    private final ApplicationEventPublisher eventPublisher;
+
+    private final AiProxyRealKeyResolver realKeyResolver;
+
+    @Autowired
+    public AiProxyApiKeyServiceImpl(
+            final AiProxyApiKeyMapper mapper, final ApplicationEventPublisher 
eventPublisher,
+            final AiProxyRealKeyResolver realKeyResolver) {
+        this.mapper = mapper;
+        this.eventPublisher = eventPublisher;
+        this.realKeyResolver = realKeyResolver;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int create(final ProxyApiKeyDTO dto, final String selectorId) {
+        final ProxyApiKeyDO entity = 
ProxyApiKeyTransfer.INSTANCE.mapToEntity(dto);
+        if (StringUtils.isBlank(entity.getId())) {
+            entity.setId(UUIDUtils.getInstance().generateShortUuid());
+        }
+        if (StringUtils.isBlank(entity.getProxyApiKey())) {
+            entity.setProxyApiKey(SignUtils.generateKey());
+        }
+        // validate namespace
+        if (StringUtils.isBlank(entity.getNamespaceId())) {
+            throw new ShenyuException(ShenyuResultMessage.PARAMETER_ERROR);
+        }
+        // validate selector id
+        if (StringUtils.isBlank(selectorId)) {
+            throw new ShenyuException(ShenyuResultMessage.PARAMETER_ERROR);
+        }
+        entity.setSelectorId(selectorId);
+        // unique check for proxyApiKey if provided
+        if (StringUtils.isNotBlank(entity.getProxyApiKey()) && 
Boolean.TRUE.equals(mapper.proxyApiKeyExisted(selectorId, 
entity.getProxyApiKey()))) {
+            throw new 
ShenyuException(ShenyuResultMessage.UNIQUE_INDEX_CONFLICT_ERROR);
+        }
+        if (Objects.isNull(entity.getEnabled())) {
+            entity.setEnabled(Boolean.TRUE);
+        }
+        // back fill generated fields to response dto first
+        dto.setId(entity.getId());
+        dto.setProxyApiKey(entity.getProxyApiKey());
+        dto.setEnabled(entity.getEnabled());
+        final int rows = mapper.insert(entity);
+        publishChange(DataEventTypeEnum.CREATE, entity);
+        return rows;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int update(final ProxyApiKeyDTO dto) {
+        final ProxyApiKeyDO entity = 
ProxyApiKeyTransfer.INSTANCE.mapToEntity(dto);
+        // id is required for update
+        if (Objects.isNull(entity) || StringUtils.isBlank(entity.getId())) {
+            return 0;
+        }
+        int rows = mapper.updateSelective(entity);
+        publishChange(DataEventTypeEnum.UPDATE, entity);
+        return rows;
+    }
+
+    @Override
+    public ProxyApiKeyVO findById(final String id) {
+        final ProxyApiKeyVO vo = 
ProxyApiKeyTransfer.INSTANCE.mapToVO(mapper.selectById(id));
+        if (Objects.nonNull(vo)) {
+            final String real = 
realKeyResolver.resolveRealKey(vo.getSelectorId()).orElse(null);
+            vo.setRealApiKey(real);
+        }
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int delete(final List<String> ids) {
+        final List<ProxyApiKeyDO> toDelete = mapper.selectByIds(ids);
+        int rows = mapper.deleteByIds(ids);
+        if (rows > 0 && Objects.nonNull(toDelete) && !toDelete.isEmpty()) {
+            publishChangeList(DataEventTypeEnum.DELETE, toDelete);
+        }
+        return rows;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String enabled(final List<String> ids, final Boolean enabled) {
+        if (Objects.isNull(ids) || ids.isEmpty() || Objects.isNull(enabled)) {
+            return ShenyuResultMessage.PARAMETER_ERROR;
+        }
+        int rows = mapper.updateEnableBatch(ids, enabled);
+        if (rows > 0) {
+            final List<ProxyApiKeyDO> updated = mapper.selectByIds(ids);
+            if (Objects.nonNull(updated) && !updated.isEmpty()) {
+                for (ProxyApiKeyDO e : updated) {
+                    e.setEnabled(enabled);
+                }
+                publishChangeList(DataEventTypeEnum.UPDATE, updated);
+            }
+            return StringUtils.EMPTY;
+        }
+        return AdminConstants.ID_NOT_EXIST;
+    }
+
+    @Override
+    public CommonPager<ProxyApiKeyVO> listByPage(final ProxyApiKeyQuery query) 
{
+        final int current = query.getPageParameter().getCurrentPage();
+        final int size = query.getPageParameter().getPageSize();
+        PageHelper.startPage(current, size);
+        final List<ProxyApiKeyVO> list = mapper.selectByCondition(query);
+        final PageInfo<ProxyApiKeyVO> pageInfo = new PageInfo<>(list);
+        return new CommonPager<>(new PageParameter(current, size, (int) 
pageInfo.getTotal()), list);
+    }
+
+    @Override
+    public List<ProxyApiKeyVO> searchByCondition(final ProxyApiKeyQuery 
condition) {
+        return mapper.selectByCondition(condition);
+    }
+
+    // =====================  sync & listAll  =====================
+
+    @Override
+    public List<ProxyApiKeyData> listAll() {
+        List<ProxyApiKeyData> list = 
mapper.selectAll().stream().map(this::convert).collect(Collectors.toList());
+        LOG.info("[AiProxySync] listAll size:{}", list.size());
+        return list;
+    }
+
+    @Override
+    public void syncData() {
+        // group by namespace and publish REFRESH respectively
+        List<ProxyApiKeyDO> all = mapper.selectAll();
+        if (Objects.isNull(all) || all.isEmpty()) {
+            return;
+        }
+        LOG.info("[AiProxySync] syncData triggered, total records:{}", 
all.size());
+        all.stream()
+                .collect(Collectors.groupingBy(ProxyApiKeyDO::getNamespaceId))
+                .values()
+                .forEach(list -> publishRefresh(list));
+    }
+
+    @Override
+    public void syncDataByNamespaceId(final String namespaceId) {
+        final String target = normalizeNamespace(namespaceId);
+        List<ProxyApiKeyDO> all = mapper.selectAll();
+        List<ProxyApiKeyDO> list = Objects.isNull(all) ? 
java.util.Collections.emptyList()
+                : all.stream()
+                .filter(e -> 
StringUtils.equals(normalizeNamespace(e.getNamespaceId()), target))
+                .collect(Collectors.toList());
+        LOG.info("[AiProxySync] syncDataByNamespaceId {}, normalized:{}, 
matched:{} of total:{}",
+                namespaceId, target, list.size(), Objects.isNull(all) ? 0 : 
all.size());
+        if (list.isEmpty()) {
+            return;
+        }
+        publishRefresh(list);
+    }
+
+    // =====================  private utils  =====================

Review Comment:
   change the comment format



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SyncDataServiceImpl.java:
##########
@@ -205,6 +217,10 @@ public boolean syncPluginData(final String namespaceId, 
final String pluginId) {
             
             eventPublisher.publishEvent(new 
DataChangedEvent(ConfigGroupEnum.RULE, DataEventTypeEnum.REFRESH, 
allRuleDataList));
         }
+        if ("aiProxy".equalsIgnoreCase(namespacePluginVO.getName())) {

Review Comment:
   use constant



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AiProxyApiKeyController.java:
##########
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.aspect.annotation.RestApi;
+import org.apache.shenyu.admin.model.dto.BatchCommonDTO;
+import org.apache.shenyu.admin.model.dto.ProxyApiKeyDTO;
+import org.apache.shenyu.admin.model.page.CommonPager;
+import org.apache.shenyu.admin.model.page.PageParameter;
+import org.apache.shenyu.admin.model.query.ProxyApiKeyQuery;
+import org.apache.shenyu.admin.model.result.ShenyuAdminResult;
+import org.apache.shenyu.admin.model.vo.ProxyApiKeyVO;
+import org.apache.shenyu.admin.service.AiProxyApiKeyService;
+import org.apache.shenyu.admin.utils.ShenyuResultMessage;
+import org.apache.shenyu.admin.service.PageService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+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.RequestParam;
+
+import org.apache.shenyu.admin.model.dto.BatchIdsDTO;
+import org.apache.shenyu.admin.mapper.SelectorMapper;
+import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.common.constant.AdminConstants;
+import java.util.ArrayList;
+import java.util.Objects;
+
+/** AiProxyApiKeyController. */
+@RestApi("/selector/{selectorId}/ai-proxy-apikey")
+public class AiProxyApiKeyController implements 
PagedController<ProxyApiKeyQuery, ProxyApiKeyVO> {
+
+    private final AiProxyApiKeyService aiProxyApiKeyService;
+
+    private final SelectorMapper selectorMapper;
+
+    public AiProxyApiKeyController(final AiProxyApiKeyService 
aiProxyApiKeyService, final SelectorMapper selectorMapper) {
+        this.aiProxyApiKeyService = aiProxyApiKeyService;
+        this.selectorMapper = selectorMapper;
+    }
+
+    /**
+     * Create or bind real->proxy api key mapping.
+     *
+     * @param selectorId selector id
+     * @param dto dto
+     * @return result
+     */
+    @PostMapping
+    @RequiresPermissions("system:aiProxyApiKey:add")
+    public ShenyuAdminResult create(@PathVariable("selectorId") final String 
selectorId,
+                                    @Valid @RequestBody final ProxyApiKeyDTO 
dto) {
+        // derive namespaceId from selector to avoid mismatch
+        final SelectorDO selector = selectorMapper.selectById(selectorId);
+        if (Objects.nonNull(selector)) {
+            dto.setNamespaceId(selector.getNamespaceId());
+        }
+        int rows = aiProxyApiKeyService.create(dto, selectorId);
+        if (rows > 0) {
+            final ProxyApiKeyVO vo = 
aiProxyApiKeyService.findById(dto.getId());
+            return 
ShenyuAdminResult.success(ShenyuResultMessage.CREATE_SUCCESS, vo);
+        }
+        return ShenyuAdminResult.error(ShenyuResultMessage.PARAMETER_ERROR);
+    }
+
+    /**
+     * List mappings by page.
+     *
+     * @param selectorId selector id
+     * @param namespaceId namespace id
+     * @param currentPage current page
+     * @param pageSize page size
+     * @param proxyApiKey optional filter
+     * @return list pager
+     */
+    @GetMapping
+    @RequiresPermissions("system:aiProxyApiKey:list")
+    public ShenyuAdminResult list(
+            @PathVariable("selectorId") final String selectorId,
+            @RequestParam(value = "namespaceId", required = false) final 
String namespaceId,
+            @RequestParam("currentPage") @NotNull final Integer currentPage,
+            @RequestParam("pageSize") @NotNull final Integer pageSize,
+            @RequestParam(value = "proxyApiKey", required = false) final 
String proxyApiKey) {
+        // derive namespaceId from selectorId to ensure filtering is bound to 
selector
+        final SelectorDO selector = selectorMapper.selectById(selectorId);
+        if (Objects.isNull(selector)) {
+            return ShenyuAdminResult.error(AdminConstants.ID_NOT_EXIST);
+        }
+        ProxyApiKeyQuery query = new ProxyApiKeyQuery();
+        // only filter by selectorId to ensure strict scoping
+        query.setSelectorId(selectorId);
+        query.setNamespaceId(selector.getNamespaceId());
+        query.setProxyApiKey(proxyApiKey);
+        query.setPageParameter(new PageParameter(currentPage, pageSize));
+        CommonPager<ProxyApiKeyVO> pager = 
aiProxyApiKeyService.listByPage(query);
+        return ShenyuAdminResult.success(ShenyuResultMessage.QUERY_SUCCESS, 
pager);
+    }
+
+    /**
+     * Update mapping description or enabled flag.
+     *
+     * @param selectorId selector id
+     * @param id id
+     * @param dto dto
+     * @return result
+     */
+    @PutMapping("/{id}")
+    @RequiresPermissions("system:aiProxyApiKey:edit")
+    public ShenyuAdminResult update(
+            @PathVariable("selectorId") final String selectorId,
+            @PathVariable("id") final String id, @RequestBody final 
ProxyApiKeyDTO dto) {
+        final ProxyApiKeyVO exist = aiProxyApiKeyService.findById(id);
+        if (Objects.isNull(exist)) {
+            return ShenyuAdminResult.error(AdminConstants.ID_NOT_EXIST);
+        }
+        if (!selectorId.equals(exist.getSelectorId())) {
+            return 
ShenyuAdminResult.error(AdminConstants.PROXY_SELECTOR_ID_IS_NOT_EXIST);
+        }
+        dto.setId(id);
+        int rows = aiProxyApiKeyService.update(dto);
+        return rows > 0
+                ? ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS)
+                : ShenyuAdminResult.error(ShenyuResultMessage.PARAMETER_ERROR);
+    }
+
+    /**
+     * Batch delete.
+     *
+     * @param selectorId selector id
+     * @param request ids container
+     * @return result
+     */
+    @PostMapping("/batchDelete")
+    @RequiresPermissions("system:aiProxyApiKey:delete")
+    public ShenyuAdminResult batchDelete(@PathVariable("selectorId") final 
String selectorId,
+                                         @Valid @RequestBody final BatchIdsDTO 
request) {
+        final java.util.List<String> allIds = request.getIds();
+        final java.util.List<String> validIds = new ArrayList<>();
+        if (Objects.nonNull(allIds)) {
+            for (String id : allIds) {
+                final ProxyApiKeyVO exist = aiProxyApiKeyService.findById(id);
+                if (Objects.nonNull(exist) && 
selectorId.equals(exist.getSelectorId())) {
+                    validIds.add(id);
+                }
+            }
+        }
+        if (validIds.isEmpty()) {
+            return 
ShenyuAdminResult.error(AdminConstants.PROXY_SELECTOR_ID_IS_NOT_EXIST);
+        }
+        int rows = aiProxyApiKeyService.delete(validIds);
+        return rows > 0
+                ? ShenyuAdminResult.success(ShenyuResultMessage.DELETE_SUCCESS)
+                : ShenyuAdminResult.error(ShenyuResultMessage.DELETE_FAIL);
+    }
+
+    /**
+     * Batch enable/disable.
+     *
+     * @param selectorId selector id
+     * @param batchCommonDTO ids and enabled
+     * @return result
+     */
+    @PostMapping("/batchEnabled")
+    @RequiresPermissions("system:aiProxyApiKey:disable")
+    public ShenyuAdminResult batchEnabled(@PathVariable("selectorId") final 
String selectorId,
+                                          @Valid @RequestBody final 
BatchCommonDTO batchCommonDTO) {
+        final java.util.List<String> allIds = batchCommonDTO.getIds();
+        final java.util.List<String> validIds = new ArrayList<>();
+        if (Objects.nonNull(allIds)) {

Review Comment:
   CollcetonUtils.isNotEmpty
   



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SelectorServiceImpl.java:
##########
@@ -124,6 +127,10 @@ public class SelectorServiceImpl implements 
SelectorService {
 
     private final DiscoveryProcessorHolder discoveryProcessorHolder;
 
+    @Autowired
+    @Lazy

Review Comment:
   why lazy?



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AiProxyConnectionServiceImpl.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.impl;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.mapper.AiProxyApiKeyMapper;
+import org.apache.shenyu.admin.model.entity.ProxyApiKeyDO;
+import org.apache.shenyu.admin.service.AiProxyConnectionService;
+import org.apache.shenyu.admin.service.support.AiProxyRealKeyResolver;
+import org.apache.shenyu.common.dto.ProxyApiKeyData;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * The type Ai proxy connection service.
+ */
+@Service
+public class AiProxyConnectionServiceImpl implements AiProxyConnectionService {

Review Comment:
   I don not understand what is this class`s meaning for



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to