feat: 添加批量检查编号存在性和批量插入链接任务的方法,优化链接生成逻辑,更新相关XML映射配置

This commit is contained in:
zyh
2025-10-03 11:02:39 +08:00
parent 83fd8b02d7
commit c6238277b8
6 changed files with 116 additions and 41 deletions

1
.idea/compiler.xml generated
View File

@@ -14,7 +14,6 @@
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.32/lombok-1.18.32.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.32/lombok-1.18.32.jar" />
</processorPath>
<module name="server" />
</profile>

2
.idea/misc.xml generated
View File

@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21 (2)" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -139,4 +139,14 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
@Param("region") String region,
@Param("deviceId") String deviceId,
@Param("qrExpireSeconds") int qrExpireSeconds);
/**
* 批量检查编号是否存在,返回已存在的编号列表
*/
List<String> findExistingCodeNos(@Param("codeNos") List<String> codeNos);
/**
* 批量插入链接任务
*/
int batchInsert(@Param("tasks") List<LinkTask> tasks);
}

View File

@@ -18,7 +18,7 @@ public class LinkListRequest {
@Schema(description = "每页大小", example = "20")
@Min(value = 1, message = "每页大小必须大于0")
@Max(value = 100, message = "每页大小不能超过100")
@Max(value = 10000, message = "每页大小不能超过10000")
private Integer pageSize = 20;
@Schema(description = "链接状态过滤", example = "NEW")

View File

@@ -20,8 +20,8 @@ import reactor.core.scheduler.Schedulers;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class LinkGenerationService {
@@ -95,19 +95,25 @@ public class LinkGenerationService {
linkBatchMapper.insert(batch);
// NEW状态的链接不设置过期时间只有激活使用时才设置过期时间
List<LinkTask> tasks = new ArrayList<>();
for (int i = 0; i < linkCount; i++) { // 生成linkCount个链接
// 批量生成唯一的编号
List<String> uniqueCodeNos = generateUniqueCodeNos(linkCount);
// 批量创建任务对象
List<LinkTask> tasks = new ArrayList<>(linkCount);
for (int i = 0; i < linkCount; i++) {
LinkTask t = new LinkTask();
t.setBatchId(batch.getId());
t.setAgentId(operator.getId());
t.setCodeNo(generateCodeNo());
t.setCodeNo(uniqueCodeNos.get(i));
t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
t.setExpireAt(null); // NEW状态不设置过期时间
t.setStatus("NEW");
linkTaskMapper.insert(t);
tasks.add(t);
}
// 批量插入
linkTaskMapper.batchInsert(tasks);
if (!isAdminOperator) {
// 扣点流水 + 账户余额
long before = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
@@ -140,29 +146,85 @@ public class LinkGenerationService {
private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // avoid confusing chars
private static final SecureRandom RANDOM = new SecureRandom();
private static final int MAX_RETRY_ATTEMPTS = 5; // 最大重试次数
private static final int MAX_BATCH_RETRY = 3; // 批量生成最大重试次数
private static final double CANDIDATE_MULTIPLIER = 1.2; // 候选数量系数生成比需要多20%的候选
/**
* 生成唯一的链接编号
* 利用数据库唯一约束确保编号不重复
* 批量生成唯一的链接编号
* 优化策略:一次生成多个候选编号,批量检查唯一性,减少数据库查询次数
*/
private String generateCodeNo() {
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
String codeNo = generateRandomCodeNo();
// 检查是否已存在(利用数据库唯一约束)
if (isCodeNoUnique(codeNo)) {
log.debug("生成唯一编号成功: {} (第{}次尝试)", codeNo, attempt);
return codeNo;
}
log.warn("编号{}已存在,第{}次尝试生成新编号", codeNo, attempt);
private List<String> generateUniqueCodeNos(int count) {
if (count <= 0) {
return Collections.emptyList();
}
// 如果多次重试都失败,使用时间戳后缀确保唯一性
String fallbackCodeNo = generateRandomCodeNo() + System.currentTimeMillis() % 10000;
log.warn("使用后备编号生成策略: {}", fallbackCodeNo);
return fallbackCodeNo.substring(0, Math.min(8, fallbackCodeNo.length()));
List<String> uniqueCodeNos = new ArrayList<>(count);
int remainingCount = count;
for (int attempt = 1; attempt <= MAX_BATCH_RETRY; attempt++) {
// 生成候选编号(比需要的数量多一些,以应对冲突)
int candidateCount = (int) Math.ceil(remainingCount * CANDIDATE_MULTIPLIER);
Set<String> candidates = generateRandomCodeNos(candidateCount);
if (log.isDebugEnabled()) {
log.debug("第{}次批量生成: 需要{}个,生成{}个候选编号", attempt, remainingCount, candidates.size());
}
// 批量检查哪些编号已存在
List<String> existingCodeNos = Collections.emptyList();
if (!candidates.isEmpty()) {
try {
existingCodeNos = linkTaskMapper.findExistingCodeNos(new ArrayList<>(candidates));
} catch (Exception e) {
log.warn("批量检查编号唯一性时发生异常: {}", e.getMessage());
existingCodeNos = new ArrayList<>(candidates); // 异常时保守处理,认为全部重复
}
}
Set<String> existingSet = new HashSet<>(existingCodeNos);
// 过滤出未使用的编号
List<String> availableCodeNos = candidates.stream()
.filter(code -> !existingSet.contains(code))
.limit(remainingCount)
.collect(Collectors.toList());
uniqueCodeNos.addAll(availableCodeNos);
remainingCount = count - uniqueCodeNos.size();
if (log.isDebugEnabled()) {
log.debug("第{}次批量生成结果: 获得{}个唯一编号,还需{}个",
attempt, availableCodeNos.size(), remainingCount);
}
if (remainingCount <= 0) {
log.info("成功批量生成{}个唯一编号,共尝试{}次", count, attempt);
return uniqueCodeNos;
}
}
// 如果经过多次重试仍不够,使用时间戳后缀确保唯一性
log.warn("批量生成后仍缺少{}个编号,使用后备策略生成", remainingCount);
long timestamp = System.currentTimeMillis();
for (int i = 0; i < remainingCount; i++) {
String fallbackCodeNo = generateRandomCodeNo() + (timestamp + i) % 10000;
uniqueCodeNos.add(fallbackCodeNo.substring(0, Math.min(8, fallbackCodeNo.length())));
}
return uniqueCodeNos;
}
/**
* 生成一批随机编号使用Set去重
*/
private Set<String> generateRandomCodeNos(int count) {
Set<String> codeNos = new HashSet<>(count);
// 生成稍多一些以应对Set去重
int attempts = count * 2;
for (int i = 0; i < attempts && codeNos.size() < count; i++) {
codeNos.add(generateRandomCodeNo());
}
return codeNos;
}
/**
@@ -176,20 +238,6 @@ public class LinkGenerationService {
return sb.toString();
}
/**
* 检查编号是否唯一
*/
private boolean isCodeNoUnique(String codeNo) {
try {
LinkTask existingTask = linkTaskMapper.findByCodeNo(codeNo);
return existingTask == null;
} catch (Exception e) {
log.warn("检查编号{}唯一性时发生异常: {}", codeNo, e.getMessage());
// 异常时保守处理,认为不唯一
return false;
}
}
private String generateToken() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);

View File

@@ -299,4 +299,22 @@
AND conflict_check.has_conflict IS NULL
</update>
<!-- 批量检查编号是否存在,返回已存在的编号列表 -->
<select id="findExistingCodeNos" resultType="string">
SELECT code_no FROM link_task
WHERE code_no IN
<foreach collection="codeNos" item="codeNo" open="(" close=")" separator=",">
#{codeNo}
</foreach>
</select>
<!-- 批量插入链接任务 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO link_task (batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at)
VALUES
<foreach collection="tasks" item="task" separator=",">
(#{task.batchId}, #{task.agentId}, #{task.codeNo}, #{task.tokenHash}, #{task.expireAt}, #{task.status}, #{task.region}, #{task.machineId}, #{task.loginAt}, #{task.refundAt}, #{task.revokedAt}, #{task.needRefresh}, #{task.refreshTime}, #{task.qrCreatedAt}, #{task.qrExpireAt}, #{task.firstRegionSelectAt})
</foreach>
</insert>
</mapper>