当前位置: 首页 > news >正文

《大营销平台系统设计实现》 - 营销服务 第3节:策略概率装配处理

一、本章诉求

本章节主要目标是实现抽奖策略模块的开发,包括数据库查询、策略值计算和 Redis/本地内存缓存。抽奖算法有两种思路:

  1. 空间换时间:提前计算好概率分布并存储(本地内存或 Redis),抽奖时随机定位,速度快(O(1)),适合高并发,但本地内存需要分布式同步。
  2. 时间换空间:抽奖时生成随机数并循环比对概率区间,占用内存少,但计算耗时,适合概率表过大不适合预存的场景。

选择哪种方式需根据实际需求、内存容量和并发量决定,同时需考虑缓存同步和策略变更处理。

二、需求介绍

1. 流程梳理

以用户为视角下,先进行整个流程的梳理。

整个过程会包括;抽奖策略、策略奖品、策略规则、奖品发放这些核心流程模块的使用。大家在看这个图的时候,可以配合着库表进行思考。在本节小傅哥会带着大家先实现抽奖策略的装配,用于后续抽奖时进行使用。

2.算法说明

两种抽奖算法方式:

  1. 可以根据概率值,来创建出累加的范围。如A是占10个,B的范围就是从10+40到50个,就是B。依次类推。当抽奖活动的随机值,就可以在这些区间内循环对比。
  2. 另外一种是存放到Map中,用空间换时间。这样在抽奖的时候,把随机值当索引使用,可以直接获取到对应的奖品结果。本节我们来实现第二种方式。

三、功能实现

1. 工程结构

  1. 本节需要扩展添加;Redis配置、库表操作、仓储层实现数据的调用处理。
  2. 此外本节需要的 Redis 服务,已经在 doc/dev-ops/environment 下 docker 提供安装

2. 环境安装

# 命令执行 docker-compose up -d version: '3.9' services: mysql: image: mysql:8.0.32 container_name: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: TZ: Asia/Shanghai # MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' # 可配置无密码,注意配置 SPRING_DATASOURCE_PASSWORD= MYSQL_ROOT_PASSWORD: 123456 # MYSQL_USER: xfg # MYSQL_PASSWORD: RTry78@#ww networks: - my-network depends_on: - mysql-job-dbdata ports: - "13306:3306" volumes: - ./mysql/sql:/docker-entrypoint-initdb.d healthcheck: test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] interval: 5s timeout: 10s retries: 10 start_period: 15s volumes_from: - mysql-job-dbdata # 自动加载数据 mysql-job-dbdata: image: alpine:3.18.2 container_name: mysql-job-dbdata volumes: - /var/lib/mysql # phpmyadmin https://hub.docker.com/_/phpmyadmin phpmyadmin: image: phpmyadmin:5.2.1 container_name: phpmyadmin hostname: phpmyadmin ports: - 8899:80 environment: - PMA_HOST=mysql - PMA_PORT=3306 - MYSQL_ROOT_PASSWORD=123456 depends_on: mysql: condition: service_healthy networks: - my-network # Redis redis: image: redis:7.2.0 container_name: redis restart: always hostname: redis privileged: true ports: - 16379:6379 volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf networks: - my-network healthcheck: test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 3 # RedisAdmin https://github.com/joeferner/redis-commander redis-admin: image: spryker/redis-commander:0.8.0 container_name: redis-admin hostname: redis-commander restart: always ports: - 8081:8081 environment: - REDIS_HOSTS=local:redis:6379 - HTTP_USER=admin - HTTP_PASSWORD=admin networks: - my-network depends_on: redis: condition: service_healthy networks: my-network: driver: bridge

3. 主要代码

主要逻辑

public interface IStrategyArmory { /** * 装配抽奖策略配置「触发的时机可以为活动审核通过后进行调用」 * * @param strategyId 策略ID * @return 装配结果 */ boolean assembleLotteryStrategy(Long strategyId); /** * 获取抽奖策略装配的随机结果 * * @param strategyId 策略ID * @return 抽奖结果 */ Integer getRandomAwardId(Long strategyId); }
/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略装配库(兵工厂),负责初始化策略计算 * @create 2023-12-23 10:02 */ @Slf4j @Service public class StrategyArmory implements IStrategyArmory { @Resource private IStrategyRepository repository; @Override public boolean assembleLotteryStrategy(Long strategyId) { // 1. 查询策略配置 List<StrategyAwardEntity> strategyAwardEntities = repository.queryStrategyAwardList(strategyId); // 2. 获取最小概率值 BigDecimal minAwardRate = strategyAwardEntities.stream() .map(StrategyAwardEntity::getAwardRate) .min(BigDecimal::compareTo) .orElse(BigDecimal.ZERO); // 3. 获取概率值总和 BigDecimal totalAwardRate = strategyAwardEntities.stream() .map(StrategyAwardEntity::getAwardRate) .reduce(BigDecimal.ZERO, BigDecimal::add); // 4. 用 1 % 0.0001 获得概率范围,百分位、千分位、万分位 BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING); // 5. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」 List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue()); for (StrategyAwardEntity strategyAward : strategyAwardEntities) { Integer awardId = strategyAward.getAwardId(); BigDecimal awardRate = strategyAward.getAwardRate(); // 计算出每个概率值需要存放到查找表的数量,循环填充 for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) { strategyAwardSearchRateTables.add(awardId); } } // 6. 对存储的奖品进行乱序操作 Collections.shuffle(strategyAwardSearchRateTables); // 7. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>(); for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) { shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i)); } // 8. 存放到 Redis repository.storeStrategyAwardSearchRateTable(strategyId, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable); return true; } @Override public Integer getRandomAwardId(Long strategyId) { // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 int rateRange = repository.getRateRange(strategyId); // 通过生成的随机值,获取概率值奖品查找表的结果 return repository.getStrategyAwardAssemble(strategyId, new SecureRandom().nextInt(rateRange)); } }

这部分负责:

  • 查询策略奖品配置
  • 计算最小概率和总概率
  • 生成概率查找表
  • 打乱后存到 Redis
  • 后续按随机数取奖品 ID
新增实体enrity

/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略结果实体 * @create 2023-12-23 09:13 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class AwardEntity { /** 用户ID */ private String userId; /** 奖品ID */ private Integer awardId; }
/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略奖品实体 * @create 2023-12-23 10:48 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class StrategyAwardEntity { /** 抽奖策略ID */ private Long strategyId; /** 抽奖奖品ID - 内部流转使用 */ private Integer awardId; /** 奖品库存总量 */ private Integer awardCount; /** 奖品库存剩余 */ private Integer awardCountSurplus; /** 奖品中奖概率 */ private BigDecimal awardRate; }
/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略条件实体 * @create 2023-12-23 09:10 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class StrategyConditionEntity { /** 用户ID */ private String userId; /** 策略ID */ private Integer strategyId; }

这里一些注解小白可能不太了解,我逐个讲解一下

1.@Data
  • 作用:是一个组合注解,包含以下 Lombok 功能:
    1. @Getter— 为类中所有字段生成 getter 方法。
    2. @Setter— 为非final字段生成 setter 方法。
    3. @ToString— 自动生成toString()方法。
    4. @EqualsAndHashCode— 自动生成equals()hashCode()方法。
    5. @RequiredArgsConstructor— 生成一个包含final字段和带@NonNull注解字段的构造函数。

2.@Builder
  • 作用:生成Builder 模式的代码,方便构建对象。
  • 效果

    User user = User.builder() .id(1L) .name("Alice") .email("alice@example.com") .build();
  • 特点
    • 可链式调用,赋值更清晰。
    • 避免写多个构造函数。
    • 对于参数较多的对象非常适合。

3.@AllArgsConstructor
  • 作用:为类生成一个包含所有字段的构造函数。
  • 示例

    @AllArgsConstructor class User { private Long id; private String name; } // 编译后等效于: public User(Long id, String name) { this.id = id; this.name = name; }

4.@NoArgsConstructor
  • 作用:为类生成一个无参构造函数
  • 示例

    @NoArgsConstructor class User { private Long id; private String name; } // 编译后等效于: public User() {}
  • 适用场景
    • 与 ORM 框架(如 MyBatis、Hibernate)结合时,ORM 需要无参构造函数进行对象映射。
    • @Builder配合使用时,可先无参实例化,再通过 Builder 设置属性。
新增策略服务仓储接口

/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略服务仓储接口 * @create 2023-12-23 09:33 */ public interface IStrategyRepository { List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId); void storeStrategyAwardSearchRateTable(Long strategyId, Integer rateRange, Map<Integer, Integer> strategyAwardSearchRateTable); Integer getStrategyAwardAssemble(Long strategyId, Integer rateKey); int getRateRange(Long strategyId); }

这里只是一个仓储接口,讲实际的实现和接口分开,方便之后如果替换redis的时候,不需要进行额外代码修改(比如redis替换为jedis)

redis配置

@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)

可能有小白不太明白这个注解的作用是什么

@ConfigurationProperties

1️⃣ 基本作用

  • 映射配置:将配置文件中的属性映射到类的字段上。
  • 例子
redis: sdk: config: host: 127.0.0.1 port: 6379 timeout: 2000

对应 Java 类:

import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "redis.sdk.config") public class RedisConfig { private String host; private int port; private int timeout; // getter & setter public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } }

这样 Spring Boot 会自动把redis.sdk.config.hosthost字段,redis.sdk.config.portport字段,依次映射。


2️⃣prefix = "redis.sdk.config"

  • 指定要映射的配置前缀。
  • 上面例子里,配置文件的redis.sdk.config.*都会映射到类中。

3️⃣ignoreInvalidFields = true

  • 作用:如果配置文件中有某些字段在类中没有对应的属性,忽略它们,不会抛出异常。
  • 默认值是false,如果配置文件有多余字段,Spring Boot 会报错。
  • 优点:配置文件可灵活升级或增加字段,而不会导致程序启动失败。
封装redis原生功能

package cn.bugstack.infrastructure.persistent.redis; import org.redisson.api.*; /** * Redis 服务 * * @author Fuzhengwei bugstack.cn @小傅哥 */ public interface IRedisService { /** * 设置指定 key 的值 * * @param key 键 * @param value 值 */ <T> void setValue(String key, T value); /** * 设置指定 key 的值 * * @param key 键 * @param value 值 * @param expired 过期时间 */ <T> void setValue(String key, T value, long expired); /** * 获取指定 key 的值 * * @param key 键 * @return 值 */ <T> T getValue(String key); /** * 获取队列 * * @param key 键 * @param <T> 泛型 * @return 队列 */ <T> RQueue<T> getQueue(String key); /** * 加锁队列 * * @param key 键 * @param <T> 泛型 * @return 队列 */ <T> RBlockingQueue<T> getBlockingQueue(String key); /** * 延迟队列 * * @param rBlockingQueue 加锁队列 * @param <T> 泛型 * @return 队列 */ <T> RDelayedQueue<T> getDelayedQueue(RBlockingQueue<T> rBlockingQueue); /** * 自增 Key 的值;1、2、3、4 * * @param key 键 * @return 自增后的值 */ long incr(String key); /** * 指定值,自增 Key 的值;1、2、3、4 * * @param key 键 * @return 自增后的值 */ long incrBy(String key, long delta); /** * 自减 Key 的值;1、2、3、4 * * @param key 键 * @return 自增后的值 */ long decr(String key); /** * 指定值,自增 Key 的值;1、2、3、4 * * @param key 键 * @return 自增后的值 */ long decrBy(String key, long delta); /** * 移除指定 key 的值 * * @param key 键 */ void remove(String key); /** * 判断指定 key 的值是否存在 * * @param key 键 * @return true/false */ boolean isExists(String key); /** * 将指定的值添加到集合中 * * @param key 键 * @param value 值 */ void addToSet(String key, String value); /** * 判断指定的值是否是集合的成员 * * @param key 键 * @param value 值 * @return 如果是集合的成员返回 true,否则返回 false */ boolean isSetMember(String key, String value); /** * 将指定的值添加到列表中 * * @param key 键 * @param value 值 */ void addToList(String key, String value); /** * 获取列表中指定索引的值 * * @param key 键 * @param index 索引 * @return 值 */ String getFromList(String key, int index); /** * 获取Map * * @param key 键 * @return 值 */ <K, V> RMap<K, V> getMap(String key); /** * 将指定的键值对添加到哈希表中 * * @param key 键 * @param field 字段 * @param value 值 */ void addToMap(String key, String field, String value); /** * 获取哈希表中指定字段的值 * * @param key 键 * @param field 字段 * @return 值 */ String getFromMap(String key, String field); /** * 获取哈希表中指定字段的值 * * @param key 键 * @param field 字段 * @return 值 */ <K, V> V getFromMap(String key, K field); /** * 将指定的值添加到有序集合中 * * @param key 键 * @param value 值 */ void addToSortedSet(String key, String value); /** * 获取 Redis 锁(可重入锁) * * @param key 键 * @return Lock */ RLock getLock(String key); /** * 获取 Redis 锁(公平锁) * * @param key 键 * @return Lock */ RLock getFairLock(String key); /** * 获取 Redis 锁(读写锁) * * @param key 键 * @return RReadWriteLock */ RReadWriteLock getReadWriteLock(String key); /** * 获取 Redis 信号量 * * @param key 键 * @return RSemaphore */ RSemaphore getSemaphore(String key); /** * 获取 Redis 过期信号量 * <p> * 基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。 * 同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。 * * @param key 键 * @return RPermitExpirableSemaphore */ RPermitExpirableSemaphore getPermitExpirableSemaphore(String key); /** * 闭锁 * * @param key 键 * @return RCountDownLatch */ RCountDownLatch getCountDownLatch(String key); /** * 布隆过滤器 * * @param key 键 * @param <T> 存放对象 * @return 返回结果 */ <T> RBloomFilter<T> getBloomFilter(String key); }
package cn.bugstack.infrastructure.persistent.redis; import org.redisson.api.*; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.Duration; /** * Redis 服务 - Redisson * * @author Fuzhengwei bugstack.cn @小傅哥 */ @Service("redissonService") public class RedissonService implements IRedisService { @Resource private RedissonClient redissonClient; public <T> void setValue(String key, T value) { redissonClient.<T>getBucket(key).set(value); } @Override public <T> void setValue(String key, T value, long expired) { RBucket<T> bucket = redissonClient.getBucket(key); bucket.set(value, Duration.ofMillis(expired)); } public <T> T getValue(String key) { return redissonClient.<T>getBucket(key).get(); } @Override public <T> RQueue<T> getQueue(String key) { return redissonClient.getQueue(key); } @Override public <T> RBlockingQueue<T> getBlockingQueue(String key) { return redissonClient.getBlockingQueue(key); } @Override public <T> RDelayedQueue<T> getDelayedQueue(RBlockingQueue<T> rBlockingQueue) { return redissonClient.getDelayedQueue(rBlockingQueue); } @Override public long incr(String key) { return redissonClient.getAtomicLong(key).incrementAndGet(); } @Override public long incrBy(String key, long delta) { return redissonClient.getAtomicLong(key).addAndGet(delta); } @Override public long decr(String key) { return redissonClient.getAtomicLong(key).decrementAndGet(); } @Override public long decrBy(String key, long delta) { return redissonClient.getAtomicLong(key).addAndGet(-delta); } @Override public void remove(String key) { redissonClient.getBucket(key).delete(); } @Override public boolean isExists(String key) { return redissonClient.getBucket(key).isExists(); } public void addToSet(String key, String value) { RSet<String> set = redissonClient.getSet(key); set.add(value); } public boolean isSetMember(String key, String value) { RSet<String> set = redissonClient.getSet(key); return set.contains(value); } public void addToList(String key, String value) { RList<String> list = redissonClient.getList(key); list.add(value); } public String getFromList(String key, int index) { RList<String> list = redissonClient.getList(key); return list.get(index); } @Override public <K, V> RMap<K, V> getMap(String key) { return redissonClient.getMap(key); } public void addToMap(String key, String field, String value) { RMap<String, String> map = redissonClient.getMap(key); map.put(field, value); } public String getFromMap(String key, String field) { RMap<String, String> map = redissonClient.getMap(key); return map.get(field); } @Override public <K, V> V getFromMap(String key, K field) { return redissonClient.<K, V>getMap(key).get(field); } public void addToSortedSet(String key, String value) { RSortedSet<String> sortedSet = redissonClient.getSortedSet(key); sortedSet.add(value); } @Override public RLock getLock(String key) { return redissonClient.getLock(key); } @Override public RLock getFairLock(String key) { return redissonClient.getFairLock(key); } @Override public RReadWriteLock getReadWriteLock(String key) { return redissonClient.getReadWriteLock(key); } @Override public RSemaphore getSemaphore(String key) { return redissonClient.getSemaphore(key); } @Override public RPermitExpirableSemaphore getPermitExpirableSemaphore(String key) { return redissonClient.getPermitExpirableSemaphore(key); } @Override public RCountDownLatch getCountDownLatch(String key) { return redissonClient.getCountDownLatch(key); } @Override public <T> RBloomFilter<T> getBloomFilter(String key) { return redissonClient.getBloomFilter(key); } }

直接用 RedissonService 行不行?行。
比如直接在 StrategyRepository 里注入:

@Resource private RedissonService redissonService;

项目一样能跑。
如果这是个很小的项目、短周期 demo,这么写也完全正常。
那为什么还要 IRedisService
主要是为了解耦,不是为了“现在必须有多个实现”
1.上层依赖抽象,不依赖具体实现
2.后面换 Redis 客户端时改动小

策略服务仓储实现

@Repository public class StrategyRepository implements IStrategyRepository { @Resource private IStrategyAwardDao strategyAwardDao; @Resource private IRedisService redisService; @Override public List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId) { // 优先从缓存获取 String cacheKey = Constants.RedisKey.STRATEGY_AWARD_KEY + strategyId; List<StrategyAwardEntity> strategyAwardEntities = redisService.getValue(cacheKey); if (null != strategyAwardEntities && !strategyAwardEntities.isEmpty()) return strategyAwardEntities; // 从库中获取数据 List<StrategyAward> strategyAwards = strategyAwardDao.queryStrategyAwardListByStrategyId(strategyId); strategyAwardEntities = new ArrayList<>(strategyAwards.size()); for (StrategyAward strategyAward : strategyAwards) { StrategyAwardEntity strategyAwardEntity = StrategyAwardEntity.builder() .strategyId(strategyAward.getStrategyId()) .awardId(strategyAward.getAwardId()) .awardCount(strategyAward.getAwardCount()) .awardCountSurplus(strategyAward.getAwardCountSurplus()) .awardRate(strategyAward.getAwardRate()) .build(); strategyAwardEntities.add(strategyAwardEntity); } redisService.setValue(cacheKey, strategyAwardEntities); return strategyAwardEntities; } @Override public void storeStrategyAwardSearchRateTable(Long strategyId, Integer rateRange, Map<Integer, Integer> strategyAwardSearchRateTable) { // 1. 存储抽奖策略范围值,如10000,用于生成1000以内的随机数 redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + strategyId, rateRange); // 2. 存储概率查找表 Map<Integer, Integer> cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + strategyId); cacheRateTable.putAll(strategyAwardSearchRateTable); } @Override public Integer getStrategyAwardAssemble(Long strategyId, Integer rateKey) { return redisService.getFromMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + strategyId, rateKey); } @Override public int getRateRange(Long strategyId) { return redisService.getValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + strategyId); } }

核心方法:

讲对应的map存入redis,方便后面取出对应的奖励

Repository 的作用,核心就两点:
1.把这个类注册成 Spring Bean
2.表示这是“持久化层/仓储层”的组件

除了语义之外,Spring 对 @Repository 还有一个经典用途:异常转换
它会把底层持久化相关异常,转换成 Spring 的统一数据访问异常体系 DataAccessException。

把零件讲完了,再具体讲一下主流程吧

附言:感觉这里的乱序没有意义,因为是生成随机数去找对应奖品

http://www.cnnetsun.cn/news/2478654.html

相关文章:

  • 通过 curl 命令快速测试 Taotoken 大模型接口连通性
  • 3步完成IDM永久免费使用:开源激活脚本完全解析
  • 如何快速将B站缓存视频转换为MP4:m4s-converter完整使用教程
  • IDM激活脚本终极指南:如何免费锁定30天试用期无限使用
  • Buzz语音转文字工具中Faster Whisper模型下载失败的3步解决方案与深度解析
  • 别折腾小米电脑管家了!用这个锤子遗产HandShaker修改版,Win/Mac轻松访问安卓14手机文件
  • 从面积与性能权衡出发:深度解析Tessent MBIST中Bypass/Observation逻辑的配置艺术
  • 智能车竞赛光电组核心技术解析:从图像处理到PID控制实战
  • Cat-Catch资源嗅探工具:5步解锁网页媒体下载新境界
  • 2026四大便利店收银软件深度横评:从参数实测到选型避坑指南
  • 3分钟掌握Blender四边形重拓扑:QRemeshify终极简单指南
  • OpenCATS:如何构建企业级招聘自动化平台
  • CANN/Ascend C矩阵乘法Tiling参数获取接口
  • 深入解析设备树二进制(DTB)格式:从内核启动到驱动绑定的底层原理
  • 3个关键决策:为什么顶级技术团队选择Arco Design Pro构建企业级应用
  • AI Cover技术深度解析:从OpenAI到AWS S3的完整架构实现
  • 告别Eclipse插件!在Maven项目中用antlr4-maven-plugin自动生成解析器代码(附完整pom.xml配置)
  • 基于容器化技术构建安全高效的Linux在线调试环境方案
  • FreeRTOS互斥锁的‘坑’与‘宝’:优先级翻转那些事儿,用ESP32实测给你看
  • 2026年大厂Java面试高频场景题 + 八股文(万字干货,纯手工硬核整理)
  • 如何快速掌握FunASR后端解码:从声学特征到文本的完整指南
  • Qlib量化投资平台:用AI技术打造智能金融分析系统的终极指南
  • 碧蓝航线Alas脚本:告别肝帝生活,让游戏自动化的终极指南
  • Linux内核启动耗时测量:从日志时间戳到硬件计数器的五种实战方法
  • WikiSQL与关系数据库的完美结合:实现自然语言接口的终极方案
  • 如何利用MaxBot自动化抢票系统高效获取热门活动门票:技术实现与实战指南
  • STM32按键消抖与状态机编程:从硬件抖动到软件架构的实战指南
  • 终极开源神器:BilibiliDown实现B站视频智能批量下载的高效解决方案
  • 手把手教你用UiAutomator2和Weditor搞定Android App元素定位与调试(Python实战)
  • 使用TaoToken快速配置ClaudeCode解决API密钥被封与Token不足问题