JobFlow 的延时调度:如何可靠地处理“30分钟后取消订单”

开源地址与系列文章

前言

前面几篇文章讲了周期任务的调度,但在实际业务中,还有另一种常见需求:

复制代码
用户下单后30分钟未支付 → 自动取消订单
支付成功后15分钟 → 发送确认短信
会员到期前3天 → 发送续费提醒

这些都不是"每天凌晨2点"这种固定周期,而是运行时动态指定的延时任务

JobFlow 的延时调度就是用来解决这个问题的。

这篇文章就来讲讲延时调度是怎么设计的。

一、延时调度 vs 定时任务

先看看两者的区别:

graph LR A[定时任务
Cron表达式] --> B[每天凌晨2点
周期执行] C[延时任务
指定时间点] --> D[30分钟后
执行一次] style A fill:#87CEEB style B fill:#90EE90 style C fill:#FFB6C1 style D fill:#FFE4B5

定时任务:

  • 配置:在数据库里配好 Cron 表达式
  • 触发:调度器按 Cron 周期性触发
  • 场景:报表生成、数据同步、对账

延时任务:

  • 配置:运行时通过 API 动态创建
  • 触发:到指定时间点执行一次
  • 场景:订单超时、消息延迟、定时提醒

对比表格:

特性 定时任务 延时任务
触发方式 周期性(Cron) 一次性(时间点)
创建方式 数据库配置 API 动态调用
重试机制 需自己实现 内置指数退避
幂等性 需自己保证 框架层面保障

二、核心设计

数据模型

延时任务存在 job_delay_task 表:

sql 复制代码
CREATE TABLE job_delay_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    trace_id VARCHAR(64) NOT NULL UNIQUE,      -- 链路追踪
    biz_uuid VARCHAR(64) NOT NULL UNIQUE,      -- 业务幂等ID
    service_name VARCHAR(100) NOT NULL,        -- 目标服务
    handler VARCHAR(100) NOT NULL,             -- Handler名称
    execute_time TIMESTAMP NOT NULL,           -- 期望执行时间
    next_attempt_time TIMESTAMP,               -- 下次尝试时间
    payload_json TEXT,                         -- 业务参数
    status VARCHAR(20) NOT NULL,               -- 状态
    retry_count INT DEFAULT 0,                 -- 重试次数
    max_retry INT DEFAULT 3,                   -- 最大重试
    ...
);

关键字段:

graph LR A[biz_uuid
业务唯一ID] --> B[幂等保证
防重复提交] C[next_attempt_time
下次尝试时间] --> D[扫描依据
决定何时执行] E[status
任务状态] --> F[状态机
驱动流转] style A fill:#87CEEB style C fill:#FFE4B5 style E fill:#FFB6C1 style B fill:#90EE90 style D fill:#90EE90 style F fill:#90EE90

biz_uuid:业务方传入的唯一ID,用于幂等

  • 示例:order-timeout-12345
  • 唯一约束:重复提交返回已有记录

next_attempt_time:核心字段,决定什么时候执行

  • 初始值:execute_time(业务期望时间)
  • 失败后:按指数退避延后(+3分钟、+5分钟)

status:状态机流转

  • PENDING:等待执行
  • SENDING:正在调用
  • SENT:执行成功
  • FAILED:失败(可重试或最终失败)

状态机

graph LR A[PENDING
等待中] --> B[SENDING
调用中] B --> C[SENT
已完成] B --> D[FAILED
失败] D --> E{重试次数?} E -->|未达上限| A E -->|达到上限| F[FAILED
最终失败] style A fill:#87CEEB style B fill:#FFE4B5 style C fill:#90EE90 style D fill:#FFB6C1 style F fill:#FF6B6B

正常流程:

objectivec 复制代码
创建任务 → PENDING
↓
时间到了 → SENDING(CAS抢占)
↓
HTTP调用 → SENT

失败重试:

ini 复制代码
HTTP调用失败 → FAILED
↓
retry_count < max_retry → 回到 PENDING
↓
next_attempt_time = now + 指数退避时间
↓
等待下次扫描

最终失败:

复制代码
retry_count >= max_retry → 标记为最终失败
→ 不再重试
→ 需要人工介入

三、创建延时任务

API调用

业务方通过HTTP接口创建延时任务:

http 复制代码
POST /api/delay-tasks
Content-Type: application/json

{
  "serviceName": "order-service",
  "handler": "orderTimeoutHandler",
  "executeTime": "2025-12-28T16:00:00",
  "bizUuid": "order-timeout-12345",
  "payloadJson": "{\"orderId\":\"12345\"}"
}

参数说明:

graph LR A[serviceName
order-service] --> B[目标服务
从Nacos发现] C[handler
orderTimeoutHandler] --> D[Handler名称
执行器实现] E[executeTime
2025-12-28 16:00] --> F[期望执行时间
到时触发] G[bizUuid
order-timeout-12345] --> H[幂等ID
防止重复] style A fill:#87CEEB style C fill:#87CEEB style E fill:#FFE4B5 style G fill:#FFB6C1 style B fill:#90EE90 style D fill:#90EE90 style F fill:#90EE90 style H fill:#90EE90

处理流程

graph LR A[接收请求] --> B[生成traceId] B --> C[封装实体] C --> D{bizUuid
已存在?} D -->|是| E[返回已有记录
幂等] D -->|否| F[插入数据库
返回新记录] style A fill:#87CEEB style B fill:#87CEEB style C fill:#87CEEB style D fill:#FFE4B5 style E fill:#90EE90 style F fill:#90EE90

核心代码:

java 复制代码
// 1. 生成 traceId
String traceId = "delay_time-" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
    .format(LocalDateTime.now()) + "-" + shortUuid();

// 2. 封装实体
JobDelayTask task = new JobDelayTask();
task.setTraceId(traceId);
task.setBizUuid(request.getBizUuid());
task.setServiceName(request.getServiceName());
task.setHandler(request.getHandler());
task.setExecuteTime(request.getExecuteTime());
task.setNextAttemptTime(request.getExecuteTime());  // 初始值
task.setPayloadJson(request.getPayloadJson());
task.setStatus("PENDING");
task.setRetryCount(0);
task.setMaxRetry(3);

// 3. 插入或获取
try {
    return delayTaskRepository.insert(task);
} catch (DuplicateKeyException e) {
    // bizUuid 冲突,返回已有记录(幂等)
    return delayTaskRepository.findByBizUuid(request.getBizUuid());
}

幂等性保证:

arduino 复制代码
业务方重复提交:
POST /api/delay-tasks(bizUuid: order-timeout-12345)
→ 第1次:插入成功,返回新记录
→ 第2次:唯一约束冲突,返回已有记录
→ 业务方得到相同的 traceId

四、扫描与调度

定时扫描

调度器每5秒扫描一次:

java 复制代码
@Scheduled(fixedDelay = 5000)
public void scanAndDispatch() {
    // 1. 扫描到期任务
    List<JobDelayTask> dueTasks = findDueTasks();
    for (JobDelayTask task : dueTasks) {
        dispatchDelayTask(task);
    }
    
    // 2. 扫描 SENDING 超时任务
    List<JobDelayTask> stuckTasks = findStuckSendingTasks();
    for (JobDelayTask task : stuckTasks) {
        handleSendingTimeout(task);
    }
}

扫描逻辑:

graph LR A[定时扫描
5秒一次] --> B[查询到期任务
PENDING/FAILED] A --> C[查询超时任务
SENDING] B --> D[逐个调度] C --> E[标记失败
进入重试] style A fill:#87CEEB style B fill:#FFE4B5 style C fill:#FFB6C1 style D fill:#90EE90 style E fill:#FFE4B5

到期任务查询:

sql 复制代码
SELECT * FROM job_delay_task 
WHERE status IN ('PENDING', 'FAILED')
  AND retry_count < max_retry
  AND next_attempt_time <= NOW()
ORDER BY next_attempt_time ASC
LIMIT 100

超时任务查询:

sql 复制代码
SELECT * FROM job_delay_task 
WHERE status = 'SENDING'
  AND TIMESTAMPDIFF(SECOND, updated_at, NOW()) > 120
LIMIT 100

无锁调度

调度分三步:Owner判定 → CAS抢占 → HTTP调用

graph LR A[扫描到任务] --> B{isOwner?
Hash分区} B -->|否| C[跳过] B -->|是| D[CAS抢占
PENDING to SENDING] D --> E{CAS成功?} E -->|否| F[其他实例抢到] E -->|是| G[HTTP调用执行器] style A fill:#87CEEB style B fill:#FFE4B5 style D fill:#87CEEB style E fill:#FFE4B5 style G fill:#90EE90 style C fill:#D3D3D3 style F fill:#D3D3D3

第一步:Owner判定

java 复制代码
// 基于 serviceName 做 Hash 分区
if (!clusterInstanceService.isOwner(task.getServiceName())) {
    return;  // 不是我负责,跳过
}

原理:

ini 复制代码
有3个调度器实例:
- scheduler-001
- scheduler-002
- scheduler-003

任务的 serviceName = "order-service"
→ hash("order-service") % 3 = 1
→ scheduler-002 负责
→ 其他实例跳过

第二步:CAS抢占

java 复制代码
// CAS 更新状态
boolean success = tryMarkSending(
    task.getId(),
    task.getStatus(),      // 期望状态:PENDING
    task.getRetryCount()   // 期望重试次数:0
);

if (!success) {
    return;  // CAS 失败,其他实例抢到了
}

SQL实现:

sql 复制代码
UPDATE job_delay_task 
SET status = 'SENDING', updated_at = NOW()
WHERE id = ? 
  AND status = ?         -- 期望状态匹配
  AND retry_count = ?    -- 期望重试次数匹配

返回 affected_rows > 0 表示抢占成功。

为什么需要CAS?

diff 复制代码
极端情况:Owner判定重叠
- 实例列表变化,两个实例都认为自己是 owner
- 同时更新同一条任务

CAS保证:
- 只有一个实例的 UPDATE 会成功
- 另一个实例的 UPDATE 返回 affected_rows = 0
- 最终只有一个实例执行

第三步:HTTP调用

java 复制代码
// 1. 负载均衡选择实例
List<ServiceInstance> instances = discoveryClient.getInstances(task.getServiceName());
int index = Math.abs(task.getTraceId().hashCode()) % instances.size();
ServiceInstance instance = instances.get(index);

// 2. 构造请求
String url = instance.getUri() + "/internal/job/" + task.getHandler();
HttpHeaders headers = new HttpHeaders();
headers.add("X-Trace-Id", task.getTraceId());
headers.add("X-Biz-UUID", task.getBizUuid());

// 3. 发送请求
HttpEntity<String> entity = new HttpEntity<>(task.getPayloadJson(), headers);
ResponseEntity<String> response = restTemplate.exchange(
    url, HttpMethod.POST, entity, String.class);

// 4. 更新状态
if (response.getStatusCode().is2xxSuccessful()) {
    markSent(task.getId());  // 标记 SENT
} else {
    handleCallFailure(task);  // 进入重试
}

五、重试机制

指数退避策略

失败后按指数退避重试:

graph LR A[首次失败
retry=0] --> B[+3分钟
retry=1] B --> C[第2次失败
retry=1] C --> D[+5分钟
retry=2] D --> E[第3次失败
retry=2] E --> F[+5分钟
retry=3] F --> G[最终失败
不再重试] style A fill:#FFB6C1 style C fill:#FFB6C1 style E fill:#FFB6C1 style G fill:#FF6B6B style B fill:#FFE4B5 style D fill:#FFE4B5 style F fill:#FFE4B5

代码逻辑:

java 复制代码
private void handleCallFailure(JobDelayTask task, String errorMsg) {
    int newRetryCount = task.getRetryCount() + 1;
    
    if (newRetryCount >= task.getMaxRetry()) {
        // 达到最大重试次数,标记最终失败
        markFailedFinal(task.getId(), newRetryCount, errorMsg);
        log.warn("延时任务最终失败,traceId={}, bizUuid={}", 
            task.getTraceId(), task.getBizUuid());
        return;
    }
    
    // 计算下次尝试时间
    LocalDateTime nextAttempt;
    if (newRetryCount == 1) {
        nextAttempt = now.plusMinutes(3);   // 第1次重试:+3分钟
    } else if (newRetryCount == 2) {
        nextAttempt = now.plusMinutes(5);   // 第2次重试:+5分钟
    } else {
        nextAttempt = now.plusMinutes(5);   // 第3次重试:+5分钟
    }
    
    // 更新为 FAILED 状态,设置 next_attempt_time
    markFailedAndScheduleNext(task.getId(), newRetryCount, nextAttempt, errorMsg);
}

时间轴示例:

makefile 复制代码
10:00:00  创建任务,execute_time = 10:30:00
10:30:00  第1次执行,失败
10:33:00  第2次执行(+3分钟),失败
10:38:00  第3次执行(+5分钟),失败
10:43:00  第4次执行(+5分钟),失败
          达到max_retry=3,标记最终失败

SENDING超时检测

有个特殊场景:HTTP调用后,一直没响应。

graph LR A[SENDING状态] --> B{超过120秒?} B -->|否| C[继续等待] B -->|是| D[标记失败
进入重试] style A fill:#FFE4B5 style B fill:#FFE4B5 style D fill:#FFB6C1 style C fill:#87CEEB

处理逻辑:

java 复制代码
private void handleSendingTimeout(JobDelayTask task) {
    String timeoutMsg = "SENDING timeout (over 120 seconds)";
    log.warn("检测到SENDING超时,traceId={}, bizUuid={}", 
        task.getTraceId(), task.getBizUuid());
    
    // 视为一次失败,进入重试流程
    handleCallFailure(task, timeoutMsg);
}

为什么需要这个检测?

复制代码
场景:HTTP调用后,执行器挂了
→ 回调不会来
→ 状态一直是 SENDING
→ 没有超时检测,任务就丢了

有了检测:
→ 120秒后发现 SENDING 超时
→ 标记失败,进入重试
→ 最多重试3次

六、业务侧怎么用

执行器实现

业务服务只需要实现一个 Handler:

java 复制代码
@Component
@JobHandler("orderTimeoutHandler")
public class OrderTimeoutHandler implements IJobHandler {
    
    @Override
    public JobResult execute(JobContext context) {
        // 1. 获取 traceId(日志追踪)
        String traceId = context.getTraceId();
        MDC.put("traceId", traceId);
        
        try {
            // 2. 解析 payload
            String payload = context.getPayload();
            JSONObject json = JSON.parseObject(payload);
            String orderId = json.getString("orderId");
            
            // 3. 幂等判断(重要!)
            Order order = orderRepository.findById(orderId);
            if (order.getStatus() != OrderStatus.UNPAID) {
                log.info("订单状态已变更,跳过取消,orderId={}", orderId);
                return JobResult.success("Order already paid");
            }
            
            // 4. 执行业务逻辑
            order.setStatus(OrderStatus.CANCELED);
            order.setCancelReason("Payment timeout");
            orderRepository.save(order);
            
            log.info("订单超时自动取消,orderId={}", orderId);
            return JobResult.success("Order canceled");
            
        } catch (Exception e) {
            log.error("取消订单失败", e);
            return JobResult.fail("Cancel failed: " + e.getMessage());
        } finally {
            MDC.clear();
        }
    }
}

关键点:

关键点一:幂等判断

kotlin 复制代码
可能重复执行的场景:
- 框架重试(失败后重试3次)
- 业务方重复提交(相同bizUuid)

必须在业务逻辑里判断:
if (order.getStatus() != UNPAID) {
    return success;  // 已经处理过了
}

关键点二:traceId传递

css 复制代码
调度器生成 traceId
→ HTTP Header 传递给执行器
→ 执行器写入 MDC
→ 所有日志自动带上 traceId
→ ELK 里搜 traceId,看到完整链路

关键点三:明确返回结果

kotlin 复制代码
成功:return JobResult.success("Order canceled");
失败:return JobResult.fail("Cancel failed: " + e.getMessage());

→ 调度器根据返回结果决定:标记 SENT 还是进入重试

完整示例

订单超时取消的完整流程:

graph LR A[用户下单
10:00:00] --> B[创建延时任务
executeTime=10:30] B --> C[调度器扫描
10:30:00] C --> D[HTTP调用
orderTimeoutHandler] D --> E[检查订单状态] E --> F{已支付?} F -->|是| G[跳过取消
返回SUCCESS] F -->|否| H[取消订单
返回SUCCESS] G --> I[标记SENT] H --> I style A fill:#87CEEB style B fill:#87CEEB style C fill:#FFE4B5 style D fill:#FFE4B5 style E fill:#FFE4B5 style F fill:#FFE4B5 style G fill:#90EE90 style H fill:#90EE90 style I fill:#90EE90

代码实现:

java 复制代码
// 1. 用户下单时创建延时任务
public Order createOrder(OrderRequest request) {
    // 创建订单
    Order order = new Order();
    order.setId(generateOrderId());
    order.setStatus(OrderStatus.UNPAID);
    order.setCreatedAt(LocalDateTime.now());
    orderRepository.save(order);
    
    // 创建延时任务(30分钟后自动取消)
    DelayTaskRequest delayTask = new DelayTaskRequest();
    delayTask.setServiceName("order-service");
    delayTask.setHandler("orderTimeoutHandler");
    delayTask.setExecuteTime(LocalDateTime.now().plusMinutes(30));
    delayTask.setBizUuid("order-timeout-" + order.getId());
    delayTask.setPayloadJson(JSON.toJSONString(
        Map.of("orderId", order.getId())
    ));
    
    delayTaskClient.createDelayTask(delayTask);
    
    return order;
}

// 2. 执行器处理超时
@JobHandler("orderTimeoutHandler")
public JobResult orderTimeout(JobContext context) {
    String orderId = JSON.parseObject(context.getPayload())
        .getString("orderId");
    
    Order order = orderRepository.findById(orderId);
    
    // 幂等判断
    if (order.getStatus() != OrderStatus.UNPAID) {
        return JobResult.success("Order already paid or canceled");
    }
    
    // 取消订单
    order.setStatus(OrderStatus.CANCELED);
    order.setCancelReason("Payment timeout");
    orderRepository.save(order);
    
    return JobResult.success("Order canceled");
}

七、设计要点

为什么用 serviceName 做 Hash 分区

延时任务的 owner 判定是按 serviceName 分区的,而不是按 taskId

java 复制代码
// 延时任务
if (!clusterInstanceService.isOwner(task.getServiceName())) {
    return;
}

// 周期任务
if (!clusterInstanceService.isOwner(job.getName())) {
    return;
}

为什么不一样?

diff 复制代码
周期任务:
- 任务是固定的(配置在数据库)
- jobName 是稳定的
- 按 jobName 分区,每个任务有固定的 owner

延时任务:
- 任务是动态的(运行时创建)
- taskId 每次都不一样
- 按 taskId 分区没意义

所以:
- 按 serviceName 分区
- 同一个服务的延时任务,由同一个调度器处理
- 减少网络开销

为什么需要双重保护

Owner判定 + CAS抢占,两层保护:

objectivec 复制代码
第一层:Owner判定(Hash分区)
→ 绝大多数情况避免冲突
→ 性能好,无需锁

第二层:CAS抢占(数据库乐观锁)
→ 极端情况的最后防线
→ 即使 owner 判定重叠,也能保证只有一个实例执行

这和周期任务的设计是一样的:无锁优先,数据库兜底。

为什么 SENDING 需要超时检测

状态机里最容易卡住的就是 SENDING 状态:

复制代码
正常流程:
PENDING → SENDING → SENT(成功)
PENDING → SENDING → FAILED(失败,重试)

卡住场景:
PENDING → SENDING → ?(执行器挂了,没回调)

没有超时检测:

复制代码
任务一直 SENDING
→ 不会重试
→ 不会标记失败
→ 任务丢了

有了超时检测:

复制代码
120秒后发现 SENDING 超时
→ 标记失败
→ 进入重试流程
→ 最多重试3次

八、监控与运维

关键指标

需要关注的指标:

graph LR A[扫描到期任务数] --> B[每次扫描100条
正常] A --> C[持续很多
积压] D[SENDING超时数] --> E[偶尔几个
正常] D --> F[大量超时
执行器异常] G[最终失败数] --> H[需要人工
介入] style A fill:#87CEEB style B fill:#90EE90 style C fill:#FF6B6B style D fill:#87CEEB style E fill:#90EE90 style F fill:#FF6B6B style G fill:#FFB6C1 style H fill:#FFE4B5

SQL查询:

sql 复制代码
-- 查询最终失败的任务(需人工介入)
SELECT * FROM job_delay_task 
WHERE status = 'FAILED' 
  AND retry_count >= max_retry
ORDER BY updated_at DESC;

-- 查询积压任务
SELECT COUNT(*) FROM job_delay_task 
WHERE status IN ('PENDING', 'FAILED')
  AND next_attempt_time <= NOW();

-- 查询 SENDING 超时任务
SELECT COUNT(*) FROM job_delay_task 
WHERE status = 'SENDING'
  AND TIMESTAMPDIFF(SECOND, updated_at, NOW()) > 120;

常见问题

问题一:任务不执行

sql 复制代码
-- 1. 查看任务状态
SELECT * FROM job_delay_task WHERE biz_uuid = 'xxx';

-- 2. 检查是否到期
SELECT next_attempt_time, NOW() FROM job_delay_task WHERE id = xxx;

-- 3. 检查重试次数
SELECT retry_count, max_retry FROM job_delay_task WHERE id = xxx;

可能原因:

  • next_attempt_time 未到期
  • status = 'SENT' 已完成
  • retry_count >= max_retry 已失败
  • 调度器未启动

问题二:任务重复执行

检查是否有重复的 bizUuid:

sql 复制代码
SELECT biz_uuid, COUNT(*) FROM job_delay_task 
GROUP BY biz_uuid 
HAVING COUNT(*) > 1;

原因:

  • 业务方未正确设置唯一的 bizUuid
  • 执行器 Handler 未实现幂等

解决:

  • 使用业务唯一ID作为 bizUuid(如 order-timeout-{orderId}
  • Handler 内部增加幂等判断

九、最佳实践

bizUuid 设计

推荐做法:

java 复制代码
// 使用业务唯一ID
String bizUuid = "order-timeout-" + orderId;
String bizUuid = "payment-remind-" + userId + "-" + paymentId;

不推荐:

java 复制代码
// 使用时间戳(不唯一)
String bizUuid = "task-" + System.currentTimeMillis();

// 使用随机UUID(无法幂等)
String bizUuid = UUID.randomUUID().toString();

executeTime 设置

推荐做法:

java 复制代码
// 基于当前时间计算
LocalDateTime executeTime = LocalDateTime.now().plusMinutes(30);

// 基于业务时间计算
LocalDateTime executeTime = order.getCreatedAt().plusMinutes(30);

不推荐:

java 复制代码
// 过去的时间(会立即执行)
LocalDateTime executeTime = LocalDateTime.now().minusMinutes(10);

Payload 设计

推荐做法:

json 复制代码
{
  "orderId": "12345",
  "action": "timeout-cancel"
}

不推荐:

json 复制代码
// 包含敏感信息
{
  "orderId": "12345",
  "password": "123456",
  "bankCard": "6222..."
}

// 数据过大
{
  "orderDetail": "超过1MB的JSON..."
}

建议:

  • Payload 只存必要的业务标识
  • 执行器内部查询完整数据
  • 避免存储敏感信息

十、总结

JobFlow 的延时调度通过以下设计实现了高可用、高性能:

核心特性:

graph LR A[无锁调度
Hash+CAS] --> B[性能好
无需分布式锁] C[自动重试
指数退避] --> D[可靠性高
最多3次] E[幂等保障
bizUuid] --> F[防重复
唯一约束] style A fill:#87CEEB style C fill:#FFE4B5 style E fill:#FFB6C1 style B fill:#90EE90 style D fill:#90EE90 style F fill:#90EE90

适用场景:

  • 订单超时处理
  • 定时提醒通知
  • 延迟消息发送
  • 异步回调重试

核心优势:

  • 无需额外中间件(如 RabbitMQ 延迟队列)
  • 复用 Nacos 服务发现,架构简洁
  • 基于 MySQL 存储,可靠性高
  • 分布式调度器,高可用

和定时任务的关系:

diff 复制代码
定时任务:周期性执行,配置固定
延时任务:一次性执行,动态创建

两者互补:
- 定时任务处理周期性业务
- 延时任务处理事件驱动的业务

JobFlow 通过统一的架构、统一的 traceId、统一的执行器接口,把这两种调度需求优雅地整合在一起。

这就是 JobFlow 延时调度的设计和实现。

相关推荐
侧耳倾听1112 小时前
RESTful API介绍
后端·restful
百***07452 小时前
从零上手 Mistral 3:开源模型接入实战 + 全场景落地指南
开源
vipbic2 小时前
基于 Nuxt 4 + Strapi 5 构建高性能 AI 导航站
前端·后端
API开发平台3 小时前
接口开发开源平台 Crabc 3.5.4 发布
低代码·开源
LuckyDog06233 小时前
性能监控专栏需求内容
开源
老华带你飞3 小时前
电商系统|基于java + vue电商系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
OC溥哥9993 小时前
2D我的世界创造模式网页版正式出炉——《我们的2D创造世界:无限创意,多人同乐》欢迎来到ourcraft.xin网站上玩
后端·python·阿里云·flask·html·游戏程序
laocooon5238578864 小时前
Rust 编程语言教学目录
开发语言·后端·rust
小希smallxi4 小时前
Rust语言入门
开发语言·后端·rust