完成前后端紫鸟部分开发
This commit is contained in:
+4
-4
@@ -6,11 +6,11 @@ import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||
import java.util.List;
|
||||
|
||||
public interface ZiniaoClient {
|
||||
Long getCompanyIdByApiKey();
|
||||
Long getCompanyIdByApiKey(String apiKey);
|
||||
|
||||
List<ZiniaoStaffItemVo> listStaff(Long companyId);
|
||||
List<ZiniaoStaffItemVo> listStaff(String apiKey, Long companyId);
|
||||
|
||||
List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId);
|
||||
List<ZiniaoShopCacheDto> listUserStores(String apiKey, Long companyId, Long userId);
|
||||
|
||||
String getUserLoginToken(Long companyId, Long userId);
|
||||
String getUserLoginToken(String apiKey, Long companyId, Long userId);
|
||||
}
|
||||
|
||||
+21
-14
@@ -26,8 +26,8 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public Long getCompanyIdByApiKey() {
|
||||
String raw = getWithApiKey("/app/builtin/company", "获取 companyId");
|
||||
public Long getCompanyIdByApiKey(String apiKey) {
|
||||
String raw = getWithApiKey(apiKey, "/app/builtin/company", "获取 companyId");
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(raw);
|
||||
JsonNode data = firstNonNull(root.get("data"), root.get("result"), root);
|
||||
@@ -44,8 +44,8 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ZiniaoStaffItemVo> listStaff(Long companyId) {
|
||||
String raw = postWithApiKey(ziniaoProperties.getStaffListPath(), Map.of(
|
||||
public List<ZiniaoStaffItemVo> listStaff(String apiKey, Long companyId) {
|
||||
String raw = postWithApiKey(apiKey, ziniaoProperties.getStaffListPath(), Map.of(
|
||||
"companyId", String.valueOf(companyId),
|
||||
"level", "",
|
||||
"isAccurate", "",
|
||||
@@ -75,8 +75,8 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId) {
|
||||
String raw = postWithApiKey(ziniaoProperties.getUserStoresPath(), Map.of(
|
||||
public List<ZiniaoShopCacheDto> listUserStores(String apiKey, Long companyId, Long userId) {
|
||||
String raw = postWithApiKey(apiKey, ziniaoProperties.getUserStoresPath(), Map.of(
|
||||
"companyId", String.valueOf(companyId),
|
||||
"isAccurate", "",
|
||||
"limit", "100",
|
||||
@@ -88,8 +88,8 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserLoginToken(Long companyId, Long userId) {
|
||||
String raw = postWithApiKey(ziniaoProperties.getUserLoginTokenPath(), Map.of(
|
||||
public String getUserLoginToken(String apiKey, Long companyId, Long userId) {
|
||||
String raw = postWithApiKey(apiKey, ziniaoProperties.getUserLoginTokenPath(), Map.of(
|
||||
"companyId", String.valueOf(companyId),
|
||||
"userId", String.valueOf(userId)
|
||||
), "获取员工登录 token");
|
||||
@@ -125,11 +125,10 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
|
||||
|
||||
private String getWithApiKey(String path, String action) {
|
||||
String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置");
|
||||
private String getWithApiKey(String apiKey, String path, String action) {
|
||||
String raw = getRestClient().get()
|
||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||
.headers(headers -> headers.setBearerAuth(apiKey))
|
||||
.headers(headers -> applyAuthorization(headers, apiKey))
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (req, res) -> {
|
||||
// 保留响应体,交给后续 validateSuccess 统一解析
|
||||
@@ -139,12 +138,11 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private String postWithApiKey(String path, Map<String, Object> body, String action) {
|
||||
String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置");
|
||||
private String postWithApiKey(String apiKey, String path, Map<String, Object> body, String action) {
|
||||
RestClient.RequestBodySpec request = getRestClient().post()
|
||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||
.headers(headers -> {
|
||||
headers.setBearerAuth(apiKey);
|
||||
applyAuthorization(headers, apiKey);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
});
|
||||
if (body != null) {
|
||||
@@ -182,6 +180,15 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private void applyAuthorization(org.springframework.http.HttpHeaders headers, String apiKey) {
|
||||
String token = requireText(apiKey, "紫鸟 apiKey 未配置");
|
||||
if (token.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
headers.set("Authorization", token);
|
||||
} else {
|
||||
headers.setBearerAuth(token);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSuccess(String raw, String action, String path) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(raw);
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.nanri.aiimage.modules.ziniao.memory.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.ziniao.memory.model.entity.ZiniaoMemoryStoreEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ZiniaoMemoryStoreMapper extends BaseMapper<ZiniaoMemoryStoreEntity> {
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.nanri.aiimage.modules.ziniao.memory.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("biz_ziniao_memory_store")
|
||||
public class ZiniaoMemoryStoreEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String cacheType;
|
||||
private String cacheKey;
|
||||
private String payloadJson;
|
||||
private LocalDateTime expiresAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.nanri.aiimage.modules.ziniao.memory.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.ziniao.memory.mapper.ZiniaoMemoryStoreMapper;
|
||||
import com.nanri.aiimage.modules.ziniao.memory.model.entity.ZiniaoMemoryStoreEntity;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ZiniaoMemoryStoreService {
|
||||
|
||||
private final ZiniaoMemoryStoreMapper ziniaoMemoryStoreMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public <T> Optional<T> get(String cacheType, String cacheKey, Class<T> valueType) {
|
||||
ZiniaoMemoryStoreEntity entity = findOne(cacheType, cacheKey);
|
||||
if (entity == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (isExpired(entity)) {
|
||||
ziniaoMemoryStoreMapper.deleteById(entity.getId());
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.ofNullable(objectMapper.readValue(entity.getPayloadJson(), valueType));
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("读取紫鸟记忆存储失败");
|
||||
}
|
||||
}
|
||||
|
||||
public <T> Optional<T> get(String cacheType, String cacheKey, JavaType javaType) {
|
||||
ZiniaoMemoryStoreEntity entity = findOne(cacheType, cacheKey);
|
||||
if (entity == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (isExpired(entity)) {
|
||||
ziniaoMemoryStoreMapper.deleteById(entity.getId());
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
T value = (T) objectMapper.readValue(entity.getPayloadJson(), javaType);
|
||||
return Optional.ofNullable(value);
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("读取紫鸟记忆存储失败");
|
||||
}
|
||||
}
|
||||
|
||||
public <E> Optional<List<E>> getList(String cacheType, String cacheKey, Class<E> elementType) {
|
||||
JavaType type = objectMapper.getTypeFactory().constructCollectionType(List.class, elementType);
|
||||
return get(cacheType, cacheKey, type);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void put(String cacheType, String cacheKey, Object payload, Duration ttl) {
|
||||
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
|
||||
String normalizedKey = normalizeRequired(cacheKey, "cacheKey 不能为空");
|
||||
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
|
||||
throw new BusinessException("ttl 不合法");
|
||||
}
|
||||
String payloadJson;
|
||||
try {
|
||||
payloadJson = objectMapper.writeValueAsString(payload);
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("写入紫鸟记忆存储失败");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expiresAt = now.plusSeconds(ttl.getSeconds());
|
||||
ZiniaoMemoryStoreEntity entity = findOne(normalizedType, normalizedKey);
|
||||
if (entity == null) {
|
||||
entity = new ZiniaoMemoryStoreEntity();
|
||||
entity.setCacheType(normalizedType);
|
||||
entity.setCacheKey(normalizedKey);
|
||||
entity.setPayloadJson(payloadJson);
|
||||
entity.setExpiresAt(expiresAt);
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
ziniaoMemoryStoreMapper.insert(entity);
|
||||
return;
|
||||
}
|
||||
entity.setPayloadJson(payloadJson);
|
||||
entity.setExpiresAt(expiresAt);
|
||||
entity.setUpdatedAt(now);
|
||||
ziniaoMemoryStoreMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(String cacheType, String cacheKey) {
|
||||
ZiniaoMemoryStoreEntity entity = findOne(cacheType, cacheKey);
|
||||
if (entity != null) {
|
||||
ziniaoMemoryStoreMapper.deleteById(entity.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int deleteExpired(int limit) {
|
||||
int safeLimit = Math.max(limit, 1);
|
||||
List<ZiniaoMemoryStoreEntity> expired = ziniaoMemoryStoreMapper.selectList(new LambdaQueryWrapper<ZiniaoMemoryStoreEntity>()
|
||||
.lt(ZiniaoMemoryStoreEntity::getExpiresAt, LocalDateTime.now())
|
||||
.orderByAsc(ZiniaoMemoryStoreEntity::getExpiresAt)
|
||||
.last("LIMIT " + safeLimit));
|
||||
if (expired.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int deleted = 0;
|
||||
for (ZiniaoMemoryStoreEntity entity : expired) {
|
||||
deleted += ziniaoMemoryStoreMapper.deleteById(entity.getId());
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
private ZiniaoMemoryStoreEntity findOne(String cacheType, String cacheKey) {
|
||||
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
|
||||
String normalizedKey = normalizeRequired(cacheKey, "cacheKey 不能为空");
|
||||
return ziniaoMemoryStoreMapper.selectOne(new LambdaQueryWrapper<ZiniaoMemoryStoreEntity>()
|
||||
.eq(ZiniaoMemoryStoreEntity::getCacheType, normalizedType)
|
||||
.eq(ZiniaoMemoryStoreEntity::getCacheKey, normalizedKey)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private boolean isExpired(ZiniaoMemoryStoreEntity entity) {
|
||||
return entity.getExpiresAt() == null || !entity.getExpiresAt().isAfter(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private String normalizeRequired(String value, String message) {
|
||||
String normalized = Objects.toString(value, "").trim();
|
||||
if (normalized.isEmpty()) {
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class ZiniaoSessionCacheDto {
|
||||
private String sessionId;
|
||||
private String apiKey;
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private String tokenType;
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.nanri.aiimage.modules.ziniao.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.shopkey.mapper.ShopKeyMapper;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopKeyEntity;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ZiniaoApiKeyProvider {
|
||||
|
||||
private final ShopKeyMapper shopKeyMapper;
|
||||
|
||||
public List<String> listApiKeys() {
|
||||
return shopKeyMapper.selectList(new LambdaQueryWrapper<ShopKeyEntity>()
|
||||
.orderByDesc(ShopKeyEntity::getId))
|
||||
.stream()
|
||||
.map(ShopKeyEntity::getZiniaoToken)
|
||||
.filter(token -> token != null && !token.isBlank())
|
||||
.map(String::trim)
|
||||
.map(token -> token.regionMatches(true, 0, "Bearer ", 0, 7) ? token.substring(7).trim() : token)
|
||||
.filter(token -> !token.isBlank())
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String getRequiredApiKey() {
|
||||
List<String> keys = listApiKeys();
|
||||
if (keys.isEmpty()) {
|
||||
throw new BusinessException("紫鸟 apiKey 未配置,请先在店铺密钥管理中维护可用令牌");
|
||||
}
|
||||
return keys.get(0);
|
||||
}
|
||||
|
||||
public boolean hasApiKey() {
|
||||
Long total = shopKeyMapper.selectCount(new LambdaQueryWrapper<ShopKeyEntity>());
|
||||
return total != null && total > 0;
|
||||
}
|
||||
}
|
||||
+244
-39
@@ -3,8 +3,10 @@ package com.nanri.aiimage.modules.ziniao.service;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.config.ZiniaoProperties;
|
||||
import com.nanri.aiimage.modules.ziniao.client.ZiniaoClient;
|
||||
import com.nanri.aiimage.modules.ziniao.memory.service.ZiniaoMemoryStoreService;
|
||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoSessionCacheDto;
|
||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoApiKeyProvider;
|
||||
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
|
||||
@@ -13,10 +15,12 @@ import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
@@ -24,8 +28,18 @@ import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ZiniaoAuthService {
|
||||
|
||||
private static final String CACHE_TYPE_COMPANY_ID = "COMPANY_ID";
|
||||
private static final String CACHE_TYPE_STAFF_LIST = "STAFF_LIST";
|
||||
private static final String CACHE_TYPE_USER_STORES = "USER_STORES";
|
||||
private static final String CACHE_TYPE_SHOP_MATCH = "SHOP_MATCH";
|
||||
private static final Duration COMPANY_ID_CACHE_TTL = Duration.ofHours(12);
|
||||
private static final Duration STAFF_LIST_CACHE_TTL = Duration.ofMinutes(30);
|
||||
private static final Duration USER_STORES_CACHE_TTL = Duration.ofMinutes(30);
|
||||
private static final Duration SHOP_MATCH_CACHE_TTL = Duration.ofMinutes(30);
|
||||
|
||||
public StoreMatchResult matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
|
||||
ensureEnabled();
|
||||
String normalizedTarget = normalizeShopName(targetShopName);
|
||||
@@ -33,38 +47,64 @@ public class ZiniaoAuthService {
|
||||
return new StoreMatchResult(false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
Long companyId = resolveCompanyId();
|
||||
List<ZiniaoStaffItemVo> staff = ziniaoClient.listStaff(companyId);
|
||||
|
||||
List<Long> userIds = new java.util.ArrayList<>();
|
||||
if (preferUserId != null && preferUserId > 0) {
|
||||
userIds.add(preferUserId);
|
||||
}
|
||||
for (ZiniaoStaffItemVo item : staff) {
|
||||
if (item != null && item.getUserId() != null && item.getUserId() > 0 && (userIds.isEmpty() || !userIds.contains(item.getUserId()))) {
|
||||
userIds.add(item.getUserId());
|
||||
List<String> apiKeys = getScanApiKeys();
|
||||
long startedAt = System.currentTimeMillis();
|
||||
int scannedKeys = 0;
|
||||
for (String apiKey : apiKeys) {
|
||||
if (isScanLimitExceeded(startedAt, scannedKeys)) {
|
||||
throw new BusinessException("紫鸟 key 轮询超出扫描限制,请缩小范围或提高扫描上限");
|
||||
}
|
||||
}
|
||||
scannedKeys++;
|
||||
|
||||
for (Long staffUserId : userIds) {
|
||||
if (staffUserId == null || staffUserId <= 0) {
|
||||
continue;
|
||||
}
|
||||
List<ZiniaoShopCacheDto> stores;
|
||||
Long companyId;
|
||||
try {
|
||||
stores = ziniaoClient.listUserStores(companyId, staffUserId);
|
||||
companyId = resolveCompanyId(apiKey);
|
||||
} catch (BusinessException ex) {
|
||||
if (isSkippableUserStoresError(ex)) {
|
||||
// key 无效/不可用时继续尝试下一个 key
|
||||
if (isInvalidApiKeyError(ex)) {
|
||||
log.info("[ziniao-match] skip invalid apiKey while resolving companyId, keyHash={}", shortKeyHash(apiKey));
|
||||
continue;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
for (ZiniaoShopCacheDto store : stores) {
|
||||
String storeName = normalizeShopName(store == null ? null : store.getShopName());
|
||||
if (!storeName.isBlank() && storeName.equals(normalizedTarget)) {
|
||||
String openStoreUrl = buildOpenStoreUrl(store.getShopId(), staffUserId, ziniaoClient.getUserLoginToken(companyId, staffUserId));
|
||||
return new StoreMatchResult(true, store.getShopId(), store.getShopName(), store.getPlatform(), staffUserId, openStoreUrl);
|
||||
StoreMatchResult cachedMatch = getCachedShopMatch(apiKey, companyId, normalizedTarget);
|
||||
if (cachedMatch != null) {
|
||||
log.info("[ziniao-match] hit shop-match cache, keyHash={}, companyId={}, shopName={}, shopId={}, userId={}",
|
||||
shortKeyHash(apiKey), companyId, normalizedTarget, cachedMatch.shopId(), cachedMatch.matchedUserId());
|
||||
String openStoreUrl = buildOpenStoreUrl(cachedMatch.shopId(), cachedMatch.matchedUserId(),
|
||||
ziniaoClient.getUserLoginToken(apiKey, companyId, cachedMatch.matchedUserId()));
|
||||
return new StoreMatchResult(true, cachedMatch.shopId(), cachedMatch.shopName(), cachedMatch.platform(), cachedMatch.matchedUserId(), openStoreUrl);
|
||||
}
|
||||
|
||||
List<ZiniaoStaffItemVo> staff = getOrLoadStaff(apiKey, companyId);
|
||||
List<Long> userIds = buildUserIds(staff, preferUserId);
|
||||
for (Long staffUserId : userIds) {
|
||||
if (staffUserId == null || staffUserId <= 0) {
|
||||
continue;
|
||||
}
|
||||
List<ZiniaoShopCacheDto> stores;
|
||||
try {
|
||||
stores = getOrLoadUserStores(apiKey, companyId, staffUserId);
|
||||
} catch (BusinessException ex) {
|
||||
if (isSkippableUserStoresError(ex)) {
|
||||
continue;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
for (ZiniaoShopCacheDto store : stores) {
|
||||
String storeName = normalizeShopName(store == null ? null : store.getShopName());
|
||||
if (!storeName.isBlank() && storeName.equals(normalizedTarget)) {
|
||||
log.info("[ziniao-match] matched via upstream scan, keyHash={}, companyId={}, shopName={}, shopId={}, userId={}",
|
||||
shortKeyHash(apiKey), companyId, normalizedTarget, store.getShopId(), staffUserId);
|
||||
cacheShopMatch(apiKey, companyId, normalizedTarget,
|
||||
new StoreMatchResult(true, store.getShopId(), store.getShopName(), store.getPlatform(), staffUserId, null),
|
||||
SHOP_MATCH_CACHE_TTL);
|
||||
String openStoreUrl = buildOpenStoreUrl(store.getShopId(), staffUserId,
|
||||
ziniaoClient.getUserLoginToken(apiKey, companyId, staffUserId));
|
||||
return new StoreMatchResult(true, store.getShopId(), store.getShopName(), store.getPlatform(), staffUserId, openStoreUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +138,8 @@ public class ZiniaoAuthService {
|
||||
private final ZiniaoProperties ziniaoProperties;
|
||||
private final ZiniaoClient ziniaoClient;
|
||||
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
||||
private final ZiniaoMemoryStoreService ziniaoMemoryStoreService;
|
||||
private final ZiniaoApiKeyProvider ziniaoApiKeyProvider;
|
||||
|
||||
public ZiniaoSessionVo getSession(String sessionId, Long userId) {
|
||||
ensureEnabled();
|
||||
@@ -122,15 +164,18 @@ public class ZiniaoAuthService {
|
||||
public ZiniaoStaffListVo listStaff() {
|
||||
ensureEnabled();
|
||||
ZiniaoStaffListVo vo = new ZiniaoStaffListVo();
|
||||
Long companyId = resolveCompanyIdForStaff();
|
||||
vo.getItems().addAll(ziniaoClient.listStaff(companyId));
|
||||
String apiKey = resolveAvailableApiKey();
|
||||
Long companyId = resolveCompanyIdForStaff(apiKey);
|
||||
vo.getItems().addAll(ziniaoClient.listStaff(apiKey, companyId));
|
||||
return vo;
|
||||
}
|
||||
|
||||
public ZiniaoShopListVo listShops(String sessionId, Long userId) {
|
||||
ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId);
|
||||
Long currentUserId = requireUserId(session.getCurrentUserId());
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||
String apiKey = resolveSessionApiKey(session);
|
||||
Long companyId = resolveCompanyId(apiKey);
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(apiKey, companyId, currentUserId);
|
||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||
if (!shops.isEmpty() && (session.getDefaultShopId() == null || session.getDefaultShopId().isBlank())) {
|
||||
session.setDefaultShopId(shops.get(0).getShopId());
|
||||
@@ -151,10 +196,12 @@ public class ZiniaoAuthService {
|
||||
public ZiniaoOpenShopVo openShop(ZiniaoOpenShopRequest request) {
|
||||
ZiniaoSessionCacheDto session = requireOrInitSession(request.getSessionId(), request.getUserId());
|
||||
Long currentUserId = requireUserId(request.getUserId() != null ? request.getUserId() : session.getCurrentUserId());
|
||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
||||
String apiKey = resolveSessionApiKey(session);
|
||||
Long companyId = resolveCompanyId(apiKey);
|
||||
String userToken = ziniaoClient.getUserLoginToken(apiKey, companyId, currentUserId);
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(session.getSessionId());
|
||||
if (shops.isEmpty()) {
|
||||
shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||
shops = ziniaoClient.listUserStores(apiKey, companyId, currentUserId);
|
||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||
}
|
||||
ZiniaoShopCacheDto targetShop = shops.stream()
|
||||
@@ -178,7 +225,8 @@ public class ZiniaoAuthService {
|
||||
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
|
||||
ensureEnabled();
|
||||
Long userId = parseConfiguredOpenStoreUserId();
|
||||
return ziniaoClient.listUserStores(resolveCompanyId(), userId);
|
||||
String apiKey = resolveAvailableApiKey();
|
||||
return ziniaoClient.listUserStores(apiKey, resolveCompanyId(apiKey), userId);
|
||||
}
|
||||
|
||||
public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) {
|
||||
@@ -187,7 +235,8 @@ public class ZiniaoAuthService {
|
||||
throw new BusinessException("店铺不存在");
|
||||
}
|
||||
Long userId = parseConfiguredOpenStoreUserId();
|
||||
String loginToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId);
|
||||
String apiKey = resolveAvailableApiKey();
|
||||
String loginToken = ziniaoClient.getUserLoginToken(apiKey, resolveCompanyId(apiKey), userId);
|
||||
return buildOpenStoreUrl(shop.getShopId(), userId, loginToken);
|
||||
}
|
||||
|
||||
@@ -196,7 +245,9 @@ public class ZiniaoAuthService {
|
||||
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
||||
session.setSessionId(sessionId);
|
||||
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
||||
session.setCompanyId(resolveCompanyId());
|
||||
String apiKey = resolveAvailableApiKey();
|
||||
session.setApiKey(apiKey);
|
||||
session.setCompanyId(resolveCompanyId(apiKey));
|
||||
session.setCurrentUserId(userId);
|
||||
session.setZiniaoUserId(String.valueOf(userId == null ? 0L : userId));
|
||||
session.setNickname("API Key模式");
|
||||
@@ -215,8 +266,12 @@ public class ZiniaoAuthService {
|
||||
if (userId != null && userId > 0) {
|
||||
session.setCurrentUserId(userId);
|
||||
session.setZiniaoUserId(String.valueOf(userId));
|
||||
ziniaoSessionCacheService.saveSession(session);
|
||||
}
|
||||
// 旧 session 可能没有保存 apiKey,补齐后续请求所需
|
||||
if (session.getApiKey() == null || session.getApiKey().isBlank()) {
|
||||
session.setApiKey(resolveAvailableApiKey());
|
||||
}
|
||||
ziniaoSessionCacheService.saveSession(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -257,11 +312,13 @@ public class ZiniaoAuthService {
|
||||
if (!ziniaoProperties.isEnabled()) {
|
||||
throw new BusinessException("紫鸟集成未启用,请先配置环境变量");
|
||||
}
|
||||
requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置");
|
||||
if (!ziniaoApiKeyProvider.hasApiKey()) {
|
||||
throw new BusinessException("紫鸟 apiKey 未配置,请先在店铺密钥管理中维护可用令牌");
|
||||
}
|
||||
}
|
||||
|
||||
private Long resolveCompanyIdForStaff() {
|
||||
return resolveCompanyId();
|
||||
private Long resolveCompanyIdForStaff(String apiKey) {
|
||||
return resolveCompanyId(apiKey);
|
||||
}
|
||||
|
||||
private Long requireUserId(Long userId) {
|
||||
@@ -271,15 +328,163 @@ public class ZiniaoAuthService {
|
||||
return userId;
|
||||
}
|
||||
|
||||
private Long resolveCompanyId() {
|
||||
private Long resolveCompanyId(String apiKey) {
|
||||
if (ziniaoProperties.getCompanyId() != null && ziniaoProperties.getCompanyId() > 0) {
|
||||
return ziniaoProperties.getCompanyId();
|
||||
}
|
||||
return ziniaoClient.getCompanyIdByApiKey();
|
||||
|
||||
String normalizedApiKey = requireText(apiKey, "紫鸟 apiKey 未配置");
|
||||
String cacheKey = buildApiKeyHash(normalizedApiKey);
|
||||
Long cached = ziniaoMemoryStoreService.get(CACHE_TYPE_COMPANY_ID, cacheKey, Long.class).orElse(null);
|
||||
if (cached != null && cached > 0) {
|
||||
log.info("[ziniao-company] hit cache, keyHash={}, companyId={}", shortKeyHash(apiKey), cached);
|
||||
return cached;
|
||||
}
|
||||
log.info("[ziniao-company] cache miss, requesting upstream, keyHash={}", shortKeyHash(apiKey));
|
||||
Long companyId = ziniaoClient.getCompanyIdByApiKey(normalizedApiKey);
|
||||
ziniaoMemoryStoreService.put(CACHE_TYPE_COMPANY_ID, cacheKey, companyId, COMPANY_ID_CACHE_TTL);
|
||||
log.info("[ziniao-company] cached upstream result, keyHash={}, companyId={}", shortKeyHash(apiKey), companyId);
|
||||
return companyId;
|
||||
}
|
||||
|
||||
private Long requireCompanyId() {
|
||||
return resolveCompanyId();
|
||||
|
||||
private List<ZiniaoStaffItemVo> getOrLoadStaff(String apiKey, Long companyId) {
|
||||
String cacheKey = buildApiKeyHash(apiKey) + ":" + companyId;
|
||||
List<ZiniaoStaffItemVo> cached = ziniaoMemoryStoreService.getList(CACHE_TYPE_STAFF_LIST, cacheKey, ZiniaoStaffItemVo.class)
|
||||
.orElse(null);
|
||||
if (cached != null) {
|
||||
log.info("[ziniao-staff] hit cache, keyHash={}, companyId={}, size={}", shortKeyHash(apiKey), companyId, cached.size());
|
||||
return cached;
|
||||
}
|
||||
log.info("[ziniao-staff] cache miss, requesting upstream, keyHash={}, companyId={}", shortKeyHash(apiKey), companyId);
|
||||
List<ZiniaoStaffItemVo> staff = ziniaoClient.listStaff(apiKey, companyId);
|
||||
ziniaoMemoryStoreService.put(CACHE_TYPE_STAFF_LIST, cacheKey, staff, STAFF_LIST_CACHE_TTL);
|
||||
log.info("[ziniao-staff] cached upstream result, keyHash={}, companyId={}, size={}", shortKeyHash(apiKey), companyId, staff.size());
|
||||
return staff;
|
||||
}
|
||||
|
||||
private List<ZiniaoShopCacheDto> getOrLoadUserStores(String apiKey, Long companyId, Long userId) {
|
||||
String cacheKey = buildApiKeyHash(apiKey) + ":" + companyId + ":" + userId;
|
||||
List<ZiniaoShopCacheDto> cached = ziniaoMemoryStoreService.getList(CACHE_TYPE_USER_STORES, cacheKey, ZiniaoShopCacheDto.class)
|
||||
.orElse(null);
|
||||
if (cached != null) {
|
||||
log.info("[ziniao-stores] hit cache, keyHash={}, companyId={}, userId={}, size={}", shortKeyHash(apiKey), companyId, userId, cached.size());
|
||||
return cached;
|
||||
}
|
||||
log.info("[ziniao-stores] cache miss, requesting upstream, keyHash={}, companyId={}, userId={}", shortKeyHash(apiKey), companyId, userId);
|
||||
List<ZiniaoShopCacheDto> stores = ziniaoClient.listUserStores(apiKey, companyId, userId);
|
||||
ziniaoMemoryStoreService.put(CACHE_TYPE_USER_STORES, cacheKey, stores, USER_STORES_CACHE_TTL);
|
||||
log.info("[ziniao-stores] cached upstream result, keyHash={}, companyId={}, userId={}, size={}", shortKeyHash(apiKey), companyId, userId, stores.size());
|
||||
return stores;
|
||||
}
|
||||
|
||||
private StoreMatchResult getCachedShopMatch(String apiKey, Long companyId, String normalizedShopName) {
|
||||
StoreMatchResult cached = ziniaoMemoryStoreService.get(CACHE_TYPE_SHOP_MATCH, buildShopMatchCacheKey(apiKey, companyId, normalizedShopName), StoreMatchResult.class)
|
||||
.orElse(null);
|
||||
if (cached == null || !cached.matched() || cached.shopId() == null || cached.matchedUserId() == null) {
|
||||
return null;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
private void cacheShopMatch(String apiKey, Long companyId, String normalizedShopName, StoreMatchResult result, Duration ttl) {
|
||||
ziniaoMemoryStoreService.put(CACHE_TYPE_SHOP_MATCH, buildShopMatchCacheKey(apiKey, companyId, normalizedShopName), result, ttl);
|
||||
}
|
||||
|
||||
private String buildShopMatchCacheKey(String apiKey, Long companyId, String normalizedShopName) {
|
||||
return buildApiKeyHash(apiKey) + ":" + companyId + ":" + normalizedShopName;
|
||||
}
|
||||
|
||||
private String buildApiKeyHash(String apiKey) {
|
||||
try {
|
||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(apiKey.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("生成紫鸟 apiKey 缓存键失败");
|
||||
}
|
||||
}
|
||||
|
||||
private String shortKeyHash(String apiKey) {
|
||||
String hash = buildApiKeyHash(apiKey);
|
||||
return hash.length() <= 12 ? hash : hash.substring(0, 12);
|
||||
}
|
||||
|
||||
private List<Long> buildUserIds(List<ZiniaoStaffItemVo> staff, Long preferUserId) {
|
||||
List<Long> userIds = new java.util.ArrayList<>();
|
||||
if (preferUserId != null && preferUserId > 0) {
|
||||
userIds.add(preferUserId);
|
||||
}
|
||||
for (ZiniaoStaffItemVo item : staff) {
|
||||
if (item != null && item.getUserId() != null && item.getUserId() > 0 && (userIds.isEmpty() || !userIds.contains(item.getUserId()))) {
|
||||
userIds.add(item.getUserId());
|
||||
}
|
||||
}
|
||||
return userIds;
|
||||
}
|
||||
|
||||
private List<String> getScanApiKeys() {
|
||||
List<String> keys = ziniaoApiKeyProvider.listApiKeys();
|
||||
if (keys.isEmpty()) {
|
||||
throw new BusinessException("紫鸟 apiKey 未配置,请先在店铺密钥管理中维护可用令牌");
|
||||
}
|
||||
Integer maxKeys = ziniaoProperties.getKeyScanMaxKeys();
|
||||
if (maxKeys == null || maxKeys <= 0 || keys.size() <= maxKeys) {
|
||||
return keys;
|
||||
}
|
||||
return keys.subList(0, maxKeys);
|
||||
}
|
||||
|
||||
private String resolveAvailableApiKey() {
|
||||
BusinessException lastError = null;
|
||||
List<String> apiKeys = getScanApiKeys();
|
||||
for (String apiKey : apiKeys) {
|
||||
try {
|
||||
resolveCompanyId(apiKey);
|
||||
return apiKey;
|
||||
} catch (BusinessException ex) {
|
||||
if (isInvalidApiKeyError(ex)) {
|
||||
lastError = ex;
|
||||
continue;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
if (lastError != null) {
|
||||
throw lastError;
|
||||
}
|
||||
throw new BusinessException("未找到可用的紫鸟 apiKey");
|
||||
}
|
||||
|
||||
private String resolveSessionApiKey(ZiniaoSessionCacheDto session) {
|
||||
if (session != null && session.getApiKey() != null && !session.getApiKey().isBlank()) {
|
||||
return session.getApiKey().trim();
|
||||
}
|
||||
return resolveAvailableApiKey();
|
||||
}
|
||||
|
||||
private boolean isInvalidApiKeyError(BusinessException ex) {
|
||||
String message = ex == null ? null : ex.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return message.contains("isv.invalid-api-key")
|
||||
|| message.contains("无效的apiKey参数")
|
||||
|| message.contains("非法的参数");
|
||||
}
|
||||
|
||||
private boolean isScanLimitExceeded(long startedAt, int scannedKeys) {
|
||||
Integer maxKeys = ziniaoProperties.getKeyScanMaxKeys();
|
||||
if (maxKeys != null && maxKeys > 0 && scannedKeys >= maxKeys) {
|
||||
return true;
|
||||
}
|
||||
Integer maxSeconds = ziniaoProperties.getKeyScanMaxSeconds();
|
||||
return maxSeconds != null && maxSeconds > 0
|
||||
&& System.currentTimeMillis() - startedAt >= maxSeconds * 1000L;
|
||||
}
|
||||
|
||||
private Long parseConfiguredOpenStoreUserId() {
|
||||
|
||||
Reference in New Issue
Block a user