feat(pricing): 新增照片打印SKU及价格计算逻辑

- 添加 PHOTO_PRINT_MU 和 PHOTO_PRINT_FX 枚举类型定义
- 实现手机照片打印和特效照片打印的基础价格计算(单价×数量)
- 支持景区特定配置的价格计算逻辑
- 验证新SKU与现有 PHOTO_PRINT 的行为一致性
- 添加相关单元测试确保价格计算准确性
This commit is contained in:
2025-11-17 08:53:08 +08:00
parent dc4091e058
commit 5d5643e7d7
12 changed files with 2269 additions and 0 deletions

View File

@@ -0,0 +1,542 @@
package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.service.impl.VoucherPrintServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* 优惠券打印服务流水号生成测试
* 专门测试generateCode方法的重复率和性能
*/
@Slf4j
@SpringBootTest
public class VoucherPrintServiceCodeGenerationTest {
private static final String CODE_PREFIX = "VT";
/**
* 模拟当前的generateCode方法实现
*/
private String generateCode() {
SimpleDateFormat sdf = new SimpleDateFormat("ss");
String timestamp = sdf.format(new Date());
String randomSuffix = String.valueOf((int)(Math.random() * 100000)).formatted("%05d");
return CODE_PREFIX + timestamp + randomSuffix;
}
/**
* 测试单线程环境下1秒内生成10个流水号的重复率
*/
@Test
public void testGenerateCodeDuplicationRateInOneSecond() {
log.info("=== 开始测试1秒内生成10个流水号的重复率 ===");
int totalRounds = 1000; // 测试1000轮
int codesPerRound = 10; // 每轮生成10个流水号
int totalDuplicates = 0;
int totalCodes = 0;
for (int round = 0; round < totalRounds; round++) {
Set<String> codes = new HashSet<>();
List<String> codeList = new ArrayList<>();
// 在很短时间内生成10个流水号
for (int i = 0; i < codesPerRound; i++) {
String code = generateCode();
codes.add(code);
codeList.add(code);
}
int duplicates = codeList.size() - codes.size();
if (duplicates > 0) {
totalDuplicates += duplicates;
log.warn("第{}轮发现{}个重复: {}", round + 1, duplicates, codeList);
}
totalCodes += codesPerRound;
// 稍微休息一下,避免在完全同一时间生成
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
double duplicationRate = (double) totalDuplicates / totalCodes * 100;
log.info("=== 单线程测试结果 ===");
log.info("总轮数: {}", totalRounds);
log.info("每轮生成数: {}", codesPerRound);
log.info("总生成数: {}", totalCodes);
log.info("总重复数: {}", totalDuplicates);
log.info("重复率: {:.4f}%", duplicationRate);
// 记录一些示例生成的流水号
log.info("=== 示例流水号 ===");
for (int i = 0; i < 20; i++) {
log.info("示例{}: {}", i + 1, generateCode());
}
}
/**
* 测试严格在1秒内生成流水号的重复率
*/
@Test
public void testStrictOneSecondGeneration() {
log.info("=== 开始测试严格1秒内生成流水号重复率 ===");
int rounds = 100;
int totalDuplicates = 0;
int totalCodes = 0;
for (int round = 0; round < rounds; round++) {
Set<String> codes = new HashSet<>();
List<String> codeList = new ArrayList<>();
long startTime = System.currentTimeMillis();
// 在1秒内尽可能多地生成流水号
while (System.currentTimeMillis() - startTime < 1000) {
String code = generateCode();
codes.add(code);
codeList.add(code);
}
int duplicates = codeList.size() - codes.size();
totalDuplicates += duplicates;
totalCodes += codeList.size();
if (duplicates > 0) {
log.warn("第{}轮: 生成{}个,重复{}个,重复率{:.2f}%",
round + 1, codeList.size(), duplicates,
(double) duplicates / codeList.size() * 100);
}
// 等待下一秒开始
try {
Thread.sleep(1100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
double overallDuplicationRate = (double) totalDuplicates / totalCodes * 100;
log.info("=== 严格1秒测试结果 ===");
log.info("测试轮数: {}", rounds);
log.info("总生成数: {}", totalCodes);
log.info("总重复数: {}", totalDuplicates);
log.info("总体重复率: {:.4f}%", overallDuplicationRate);
log.info("平均每轮生成: {:.1f}个", (double) totalCodes / rounds);
}
/**
* 分析流水号的分布特征
*/
@Test
public void testCodeDistributionAnalysis() {
log.info("=== 开始分析流水号分布特征 ===");
int sampleSize = 10000;
List<String> codes = new ArrayList<>();
Map<String, Integer> prefixCount = new HashMap<>(); // 时间前缀统计
Map<String, Integer> suffixCount = new HashMap<>(); // 随机后缀统计
// 生成样本
for (int i = 0; i < sampleSize; i++) {
String code = generateCode();
codes.add(code);
// 提取时间前缀 (VTxx)
String prefix = code.substring(0, 4);
prefixCount.put(prefix, prefixCount.getOrDefault(prefix, 0) + 1);
// 提取随机后缀 (最后5位)
String suffix = code.substring(4);
suffixCount.put(suffix, suffixCount.getOrDefault(suffix, 0) + 1);
// 稍微间隔一下,避免全在同一秒
if (i % 100 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 计算去重率
Set<String> uniqueCodes = new HashSet<>(codes);
double uniqueRate = (double) uniqueCodes.size() / sampleSize * 100;
// 分析时间前缀分布
log.info("=== 时间前缀分布 (前10个) ===");
prefixCount.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(entry -> log.info("前缀 {}: {}次 ({:.2f}%)",
entry.getKey(), entry.getValue(),
(double) entry.getValue() / sampleSize * 100));
// 检查随机后缀的重复情况
long duplicatedSuffixes = suffixCount.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.count();
log.info("=== 分布分析结果 ===");
log.info("样本总数: {}", sampleSize);
log.info("唯一流水号数: {}", uniqueCodes.size());
log.info("去重率: {:.4f}%", uniqueRate);
log.info("时间前缀种类: {}", prefixCount.size());
log.info("随机后缀种类: {}", suffixCount.size());
log.info("重复的随机后缀数: {}", duplicatedSuffixes);
log.info("随机后缀重复率: {:.4f}%", (double) duplicatedSuffixes / suffixCount.size() * 100);
}
/**
* 模拟真实业务场景:快速连续请求
*/
@Test
public void testRealBusinessScenario() {
log.info("=== 开始模拟真实业务场景测试 ===");
// 模拟场景:10个用户几乎同时请求打印小票
int simultaneousUsers = 10;
int testsPerUser = 5; // 每个用户发送5次请求
int totalTests = 50; // 总共进行50次这样的场景测试
int totalDuplicates = 0;
int totalCodes = 0;
for (int test = 0; test < totalTests; test++) {
Set<String> allCodes = new HashSet<>();
List<String> allCodesList = new ArrayList<>();
// 模拟同一时刻多个用户的请求
for (int user = 0; user < simultaneousUsers; user++) {
for (int request = 0; request < testsPerUser; request++) {
String code = generateCode();
allCodes.add(code);
allCodesList.add(code);
}
}
int duplicates = allCodesList.size() - allCodes.size();
if (duplicates > 0) {
totalDuplicates += duplicates;
log.warn("第{}次场景测试发现{}个重复流水号", test + 1, duplicates);
// 找出重复的流水号
Map<String, Integer> codeCount = new HashMap<>();
for (String code : allCodesList) {
codeCount.put(code, codeCount.getOrDefault(code, 0) + 1);
}
codeCount.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.forEach(entry -> log.warn("重复流水号: {} (出现{}次)",
entry.getKey(), entry.getValue()));
}
totalCodes += allCodesList.size();
// 模拟请求间隔
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
double duplicationRate = (double) totalDuplicates / totalCodes * 100;
log.info("=== 真实业务场景测试结果 ===");
log.info("场景测试次数: {}", totalTests);
log.info("每次场景用户数: {}", simultaneousUsers);
log.info("每用户请求数: {}", testsPerUser);
log.info("总生成流水号数: {}", totalCodes);
log.info("总重复数: {}", totalDuplicates);
log.info("重复率: {:.4f}%", duplicationRate);
if (duplicationRate > 0.1) {
log.warn("警告:重复率超过0.1%,建议优化generateCode方法!");
}
}
/**
* 高并发多线程测试
*/
@Test
public void testHighConcurrencyGeneration() throws InterruptedException {
log.info("=== 开始高并发多线程测试 ===");
int threadCount = 20; // 20个并发线程
int codesPerThread = 50; // 每个线程生成50个流水号
int totalExpectedCodes = threadCount * codesPerThread;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
ConcurrentHashMap<String, Integer> allCodes = new ConcurrentHashMap<>();
List<String> allCodesList = Collections.synchronizedList(new ArrayList<>());
// 启动所有线程
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
executor.submit(() -> {
try {
List<String> threadCodes = new ArrayList<>();
// 每个线程快速生成流水号
for (int j = 0; j < codesPerThread; j++) {
String code = generateCode();
threadCodes.add(code);
allCodesList.add(code);
// 统计重复
Integer count = allCodes.put(code, 1);
if (count != null) {
allCodes.put(code, count + 1);
}
}
log.debug("线程{}完成,生成{}个流水号", threadId, threadCodes.size());
} finally {
latch.countDown();
}
});
}
// 等待所有线程完成
boolean finished = latch.await(30, TimeUnit.SECONDS);
executor.shutdown();
if (!finished) {
log.error("测试超时!");
return;
}
// 分析结果
Set<String> uniqueCodes = new HashSet<>(allCodesList);
int duplicates = totalExpectedCodes - uniqueCodes.size();
double duplicationRate = (double) duplicates / totalExpectedCodes * 100;
// 找出重复的流水号
List<Map.Entry<String, Integer>> duplicatedCodes = allCodes.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(20) // 只显示前20个最多重复的
.toList();
log.info("=== 高并发测试结果 ===");
log.info("并发线程数: {}", threadCount);
log.info("每线程生成数: {}", codesPerThread);
log.info("预期总数: {}", totalExpectedCodes);
log.info("实际总数: {}", allCodesList.size());
log.info("唯一流水号数: {}", uniqueCodes.size());
log.info("重复数: {}", duplicates);
log.info("重复率: {:.4f}%", duplicationRate);
if (!duplicatedCodes.isEmpty()) {
log.warn("=== 发现重复流水号 ===");
duplicatedCodes.forEach(entry ->
log.warn("流水号: {} 重复了 {} 次", entry.getKey(), entry.getValue()));
}
if (duplicationRate > 1.0) {
log.error("严重警告:高并发下重复率超过1.0%,必须优化generateCode方法!");
}
}
/**
* 模拟极端高压场景:短时间内大量请求
*/
@Test
public void testExtremeHighPressure() throws InterruptedException {
log.info("=== 开始极端高压测试 ===");
int threadCount = 50; // 50个并发线程
int codesPerThread = 20; // 每个线程生成20个
long timeWindowMs = 1000; // 在1秒内完成
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
ConcurrentHashMap<String, List<Integer>> codeToThreads = new ConcurrentHashMap<>();
List<String> allCodes = Collections.synchronizedList(new ArrayList<>());
// 准备所有线程
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
executor.submit(() -> {
try {
// 等待开始信号
startLatch.await();
long startTime = System.currentTimeMillis();
List<String> threadCodes = new ArrayList<>();
// 在时间窗口内尽可能快地生成
while (System.currentTimeMillis() - startTime < timeWindowMs &&
threadCodes.size() < codesPerThread) {
String code = generateCode();
threadCodes.add(code);
allCodes.add(code);
// 记录哪个线程生成了这个流水号
codeToThreads.computeIfAbsent(code, k ->
Collections.synchronizedList(new ArrayList<>())).add(threadId);
}
log.debug("线程{}在{}ms内生成{}个流水号",
threadId, System.currentTimeMillis() - startTime, threadCodes.size());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
});
}
// 开始测试
long testStartTime = System.currentTimeMillis();
startLatch.countDown();
// 等待完成
boolean finished = endLatch.await(timeWindowMs + 5000, TimeUnit.MILLISECONDS);
executor.shutdown();
long testDuration = System.currentTimeMillis() - testStartTime;
if (!finished) {
log.error("极端高压测试超时!");
return;
}
// 分析结果
Set<String> uniqueCodes = new HashSet<>(allCodes);
int totalGenerated = allCodes.size();
int duplicates = totalGenerated - uniqueCodes.size();
double duplicationRate = (double) duplicates / totalGenerated * 100;
double generationRate = (double) totalGenerated / testDuration * 1000; // 每秒生成数
// 分析重复模式
Map<String, List<Integer>> duplicatedCodes = codeToThreads.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
log.info("=== 极端高压测试结果 ===");
log.info("并发线程数: {}", threadCount);
log.info("预期每线程生成数: {}", codesPerThread);
log.info("测试持续时间: {}ms", testDuration);
log.info("实际总生成数: {}", totalGenerated);
log.info("唯一流水号数: {}", uniqueCodes.size());
log.info("重复数: {}", duplicates);
log.info("重复率: {:.4f}%", duplicationRate);
log.info("生成速率: {:.1f} codes/sec", generationRate);
if (!duplicatedCodes.isEmpty()) {
log.warn("=== 极端高压下的重复情况 ===");
duplicatedCodes.entrySet().stream()
.limit(10) // 只显示前10个
.forEach(entry -> {
String code = entry.getKey();
List<Integer> threads = entry.getValue();
log.warn("流水号: {} 被线程 {} 重复生成", code, threads);
});
}
// 评估结果
if (duplicationRate > 5.0) {
log.error("极严重警告:极端高压下重复率超过5.0%,generateCode方法不适合高并发场景!");
} else if (duplicationRate > 1.0) {
log.warn("警告:极端高压下重复率超过1.0%,建议优化generateCode方法");
}
if (generationRate > 10000) {
log.info("性能良好:生成速率超过10,000 codes/sec");
}
}
/**
* 综合测试报告
*/
@Test
public void generateComprehensiveReport() {
log.info("=== 生成综合测试报告 ===");
// 基础性能测试
long startTime = System.nanoTime();
List<String> sample = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
sample.add(generateCode());
}
long duration = System.nanoTime() - startTime;
double avgTimePerCode = duration / 1000.0 / 1_000_000; // 毫秒
// 唯一性分析
Set<String> uniqueSample = new HashSet<>(sample);
double sampleDuplicationRate = (double) (sample.size() - uniqueSample.size()) / sample.size() * 100;
// 长度和格式分析
String sampleCode = generateCode();
int codeLength = sampleCode.length();
boolean hasCorrectPrefix = sampleCode.startsWith(CODE_PREFIX);
// 理论分析
double theoreticalCollisionProbability = calculateBirthdayParadoxProbability(10, 100000);
log.info("=== generateCode方法综合评估报告 ===");
log.info("基础信息:");
log.info(" - 代码前缀: {}", CODE_PREFIX);
log.info(" - 流水号长度: {}", codeLength);
log.info(" - 格式正确: {}", hasCorrectPrefix);
log.info(" - 示例流水号: {}", sampleCode);
log.info("性能指标:");
log.info(" - 平均生成时间: {:.3f}ms", avgTimePerCode);
log.info(" - 理论最大生成速率: {:.0f} codes/sec", 1000.0 / avgTimePerCode);
log.info("唯一性分析:");
log.info(" - 样本重复率: {:.4f}% (1000个样本)", sampleDuplicationRate);
log.info(" - 理论冲突概率: {:.4f}% (1秒内10个)", theoreticalCollisionProbability * 100);
log.info(" - 随机数范围: 100,000 (00000-99999)");
log.info("风险评估:");
if (sampleDuplicationRate > 0.5) {
log.error(" - 高风险:样本重复率过高,不适合生产环境");
} else if (sampleDuplicationRate > 0.1) {
log.warn(" - 中风险:存在一定重复概率,建议优化");
} else {
log.info(" - 低风险:重复概率较低,基本可用");
}
log.info("优化建议:");
log.info(" - 建议1:使用毫秒级时间戳替代秒级");
log.info(" - 建议2:增加机器标识或进程ID");
log.info(" - 建议3:使用原子递增计数器");
log.info(" - 建议4:采用UUID算法确保全局唯一性");
}
/**
* 计算生日悖论概率
*/
private double calculateBirthdayParadoxProbability(int n, int d) {
if (n > d) return 1.0;
double probability = 1.0;
for (int i = 0; i < n; i++) {
probability *= (double) (d - i) / d;
}
return 1.0 - probability;
}
}