refactor: 后端标识统一为 FruitShorts,并落地 05 离线下载

为什么:
1) 产品已定名 FruitShorts,但后端仍是 reelshort 系列标识 —— Java 包 com.reelshort、
   模块名与 jar 名 reelshort-*、spring.application.name、配置前缀 reelshort.*,
   这些会出现在启动日志、命令行和 IDE 里,与产品名不一致。本次整体替换为 fruitshorts,
   数据库连接名上一轮已改名,保持不变。
2) 第一阶段下一批第 1 步(05 离线下载)要求把下载授权(设备绑定/有效期/吊销)、
   下载任务与断点续传、配额与过期清理、仅 Wi-Fi 规则做成完整接口,本提交一并落地。
3) 顺带修复:schema-phase1.sql 里 playback_ticket / order_event 仍按普通表定义唯一索引,
   表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就
   按分区表定义,并验证脚本可重复执行。
This commit is contained in:
Vicky committed 2026-10-15 11:44:26 +08:00
1 parent a4aa749cc6
commit b3179fc836
251 files changed
+7261 -767

No files matched your search

@@ -0,0 +1,39 @@
package com.fruitshorts.common.exception;
import lombok.Getter;
/**
* 全局错误码,详见 docs/architecture-design.md 附录 A。
*/
@Getter
public enum ErrorCode {
SUCCESS(0, "ok"),
BAD_REQUEST(400, "参数校验失败"),
UNAUTHORIZED(401, "未登录或登录已过期"),
FORBIDDEN(403, "无权限"),
NOT_FOUND(404, "资源不存在"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
INTERNAL_ERROR(500, "系统异常"),
LOGIN_FAILED(100000, "账号或密码错误"),
ACCOUNT_DISABLED(100001, "账号已停用"),
COIN_NOT_ENOUGH(100002, "金币余额不足"),
EPISODE_NOT_AVAILABLE(100003, "剧集未发布或不存在"),
EPISODE_UNLOCKED(100004, "剧集已解锁"),
ORDER_STATUS_INVALID(100005, "订单已支付或已关闭"),
VIP_EXPIRED(100006, "VIP 已过期"),
DATA_EXISTS(200001, "数据已存在"),
DATA_NOT_FOUND(200002, "数据不存在"),
OPERATION_NOT_ALLOWED(200003, "当前状态不允许该操作");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
}
@@ -0,0 +1,31 @@
package com.fruitshorts.common.exception;
import lombok.Getter;
/**
* 业务异常:带业务错误码,由全局异常处理器转成统一响应。
*/
@Getter
public class ServiceException extends RuntimeException {
private final int code;
public ServiceException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public ServiceException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public ServiceException(String message) {
super(message);
this.code = ErrorCode.INTERNAL_ERROR.getCode();
}
public static ServiceException of(ErrorCode errorCode) {
return new ServiceException(errorCode);
}
}
@@ -0,0 +1,30 @@
package com.fruitshorts.common.pojo;
import lombok.Data;
/**
* 分页请求参数,pageIndex 从 1 开始,与前端一致。
*/
@Data
public class PageParam {
private static final int MAX_PAGE_SIZE = 200;
private Integer pageIndex = 1;
private Integer pageSize = 10;
public long getOffset() {
return (long) (getSafePageIndex() - 1) * getSafePageSize();
}
public int getSafePageIndex() {
return pageIndex == null || pageIndex < 1 ? 1 : pageIndex;
}
public int getSafePageSize() {
if (pageSize == null || pageSize < 1) {
return 10;
}
return Math.min(pageSize, MAX_PAGE_SIZE);
}
}
@@ -0,0 +1,21 @@
package com.fruitshorts.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* 分页响应,字段名与前端表格组件(total / list)保持一致。
*/
@Getter
@AllArgsConstructor
public class PageResult<T> {
private final long total;
private final List<T> list;
public static <T> PageResult<T> empty() {
return new PageResult<>(0L, List.of());
}
}
@@ -0,0 +1,37 @@
package com.fruitshorts.common.pojo;
import com.fruitshorts.common.exception.ErrorCode;
import lombok.Getter;
/**
* 统一响应体,与前端 axios 拦截器约定一致:{ code, message, data }。
*/
@Getter
public class Result<T> {
private final int code;
private final String message;
private final T data;
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static Result<Void> ok() {
return new Result<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), null);
}
public static <T> Result<T> ok(T data) {
return new Result<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), data);
}
public static <T> Result<T> error(ErrorCode errorCode) {
return new Result<>(errorCode.getCode(), errorCode.getMessage(), null);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}