first commit

This commit is contained in:
2026-02-13 17:15:00 +08:00
commit cace369da2
240 changed files with 21047 additions and 0 deletions

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
# IDE
.idea/
*.iml
*.ipr
*.iws
.project
.classpath
.settings/
.vscode/
# Logs
logs/
*.log
# OS
.DS_Store
Thumbs.db
# Build
build/
out/
# Temp
*.tmp
*.bak
*.swp
# 敏感配置文件(本地环境配置不要提交)
application-local.yml
application-local.properties
*-local.yml
*-local.properties

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
# 1818AI 微信小程序后端服务
## 项目简介
基于 Spring Boot 的微信小程序后端服务。
## 技术栈
- Java
- Spring Boot
- Maven
## 快速开始
```bash
# 安装依赖
mvn install
# 启动服务
mvn spring-boot:run
```
## 目录结构
```
├── src/ # 源代码
├── docs/ # 文档
├── pom.xml # Maven 配置
└── README.md # 项目说明
```
## License
MIT

427
docs/CODE_STANDARDS.md Normal file
View File

@@ -0,0 +1,427 @@
# 代码开发规范
## 一、项目结构规范
```
src/main/java/com/dora/
├── WeixinApplication.java # 启动类
├── common/ # 公共模块
│ ├── constant/ # 常量定义
│ ├── enums/ # 枚举类
│ ├── exception/ # 异常处理
│ └── result/ # 统一响应
├── config/ # 配置类
├── controller/ # 控制器层(接收请求)
├── service/ # 业务服务层
│ └── impl/ # 服务实现
├── mapper/ # 数据访问层
├── entity/ # 数据库实体
├── dto/ # 数据传输对象(入参)
├── vo/ # 视图对象(出参)
├── util/ # 工具类
└── aspect/ # 切面
```
## 二、命名规范
### 2.1 类命名
| 类型 | 规范 | 示例 |
|------|------|------|
| Controller | XxxController | UserController |
| Service接口 | XxxService | UserService |
| Service实现 | XxxServiceImpl | UserServiceImpl |
| Mapper | XxxMapper | UserMapper |
| Entity | 与表名对应,驼峰 | User, AiWork |
| DTO | XxxDTO / XxxRequest | UserLoginDTO |
| VO | XxxVO | UserInfoVO |
| 枚举 | XxxEnum | UserStatusEnum |
| 常量 | XxxConstant | RedisConstant |
| 工具类 | XxxUtil | DateUtil |
| 配置类 | XxxConfig | RedisConfig |
### 2.2 方法命名
| 操作 | 命名规范 | 示例 |
|------|----------|------|
| 查询单个 | getXxx / findXxx | getUserById |
| 查询列表 | listXxx | listUserByStatus |
| 分页查询 | pageXxx | pageUser |
| 新增 | save / add / create | saveUser |
| 修改 | update / modify | updateUser |
| 删除 | delete / remove | deleteUser |
| 统计 | countXxx | countUserByVip |
| 判断 | isXxx / hasXxx | isVipUser |
### 2.3 变量命名
- 使用小驼峰:`userId`, `orderNo`
- 布尔类型:`isXxx`, `hasXxx`, `canXxx`
- 集合类型:复数形式 `users`, `orderList`
- 常量:全大写下划线分隔 `MAX_RETRY_COUNT`
## 三、Controller规范
```java
@Slf4j
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 获取用户信息
*
* @param id 用户ID
* @return 用户信息
*/
@GetMapping("/{id}")
public Result<UserVO> getById(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
/**
* 分页查询用户
*/
@GetMapping("/page")
public Result<IPage<UserVO>> page(UserPageDTO dto) {
return Result.success(userService.page(dto));
}
/**
* 新增用户
*/
@PostMapping
public Result<Long> save(@RequestBody @Valid UserSaveDTO dto) {
return Result.success(userService.save(dto));
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody @Valid UserUpdateDTO dto) {
userService.update(id, dto);
return Result.success();
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success();
}
}
```
### Controller规则
1. 使用 `@RequiredArgsConstructor` 构造器注入,禁止 `@Autowired`
2. 方法必须有注释说明
3. 入参使用 DTO出参使用 VO
4. 参数校验使用 `@Valid`
5. 统一返回 `Result<T>`
6. 不写业务逻辑,只做参数接收和结果返回
## 四、Service规范
```java
public interface UserService {
UserVO getById(Long id);
IPage<UserVO> page(UserPageDTO dto);
Long save(UserSaveDTO dto);
void update(Long id, UserUpdateDTO dto);
void delete(Long id);
}
```
```java
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RedisUtil redisUtil;
@Override
public UserVO getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
return convertToVO(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long save(UserSaveDTO dto) {
// 1. 参数校验
checkPhoneExists(dto.getPhone());
// 2. 构建实体
User user = new User();
BeanUtils.copyProperties(dto, user);
// 3. 保存数据
userMapper.insert(user);
// 4. 返回结果
return user.getId();
}
}
```
### Service规则
1. 接口与实现分离
2. 事务注解加在实现类方法上:`@Transactional(rollbackFor = Exception.class)`
3. 复杂业务逻辑拆分为私有方法
4. 异常使用 `BusinessException` 抛出
5. 日志记录关键操作
## 五、Entity规范
```java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String openid;
private String nickname;
private String phone;
private Integer vipLevel;
private LocalDateTime vipExpireTime;
private Integer points;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}
```
### Entity规则
1. 使用 `@Data` 简化代码
2. 字段类型与数据库对应
3. 时间类型使用 `LocalDateTime`
4. 逻辑删除字段使用 `@TableLogic`
5. 禁止在Entity中写业务方法
## 六、DTO/VO规范
```java
// 入参DTO
@Data
public class UserSaveDTO {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@NotBlank(message = "昵称不能为空")
@Size(max = 64, message = "昵称最长64个字符")
private String nickname;
}
// 出参VO
@Data
public class UserVO {
private Long id;
private String nickname;
private String avatar;
private Integer vipLevel;
private String vipLevelName;
private Integer points;
private LocalDateTime createdAt;
}
```
### DTO/VO规则
1. DTO用于接收入参必须加校验注解
2. VO用于返回数据可添加格式化字段
3. 禁止直接返回Entity
4. 敏感字段密码、token不放入VO
## 七、Mapper规范
```java
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 根据手机号查询用户
*/
User selectByPhone(@Param("phone") String phone);
/**
* 批量更新状态
*/
int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
}
```
### Mapper规则
1. 继承 `BaseMapper<T>` 使用通用方法
2. 复杂SQL写在XML文件中
3. 参数使用 `@Param` 注解
4. 禁止在Mapper中写业务逻辑
## 八、异常处理规范
```java
// 抛出业务异常
throw new BusinessException(ResultCode.USER_NOT_FOUND);
throw new BusinessException("自定义错误信息");
throw new BusinessException(4001, "自定义错误码和信息");
// 参数校验
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
// 使用断言(推荐)
Assert.notNull(user, "用户不存在");
```
## 九、日志规范
```java
@Slf4j
public class UserServiceImpl {
public void doSomething() {
// DEBUG调试信息
log.debug("查询用户参数: {}", dto);
// INFO关键业务操作
log.info("用户注册成功, userId={}, phone={}", user.getId(), user.getPhone());
// WARN警告信息
log.warn("用户登录失败次数过多, userId={}", userId);
// ERROR异常错误带异常堆栈
log.error("调用第三方接口失败, url={}", url, e);
}
}
```
### 日志规则
1. 使用 `@Slf4j` 注解
2. 使用占位符 `{}`,禁止字符串拼接
3. ERROR日志必须带异常对象
4. 敏感信息脱敏处理
## 十、数据库规范
### 10.1 连接池配置
项目使用 HikariCP 连接池,已优化配置:
- 最小空闲连接10
- 最大连接数50
- 空闲超时10分钟
- 连接最大存活30分钟
- 泄漏检测60秒
### 10.2 SQL规范
1. 禁止 `SELECT *`,明确指定字段
2. 大表查询必须走索引
3. 禁止在循环中执行SQL
4. 批量操作使用 `saveBatch`
5. 分页查询必须有排序字段
```java
// 错误示例
for (Long id : ids) {
userMapper.selectById(id); // N+1问题
}
// 正确示例
List<User> users = userMapper.selectBatchIds(ids);
```
### 10.3 事务规范
```java
// 只读事务
@Transactional(readOnly = true)
public UserVO getById(Long id) { }
// 写事务
@Transactional(rollbackFor = Exception.class)
public void save(UserDTO dto) { }
```
## 十一、接口规范
### 11.1 RESTful规范
| 操作 | 方法 | 路径 | 示例 |
|------|------|------|------|
| 查询 | GET | /资源 | GET /users |
| 详情 | GET | /资源/{id} | GET /users/1 |
| 新增 | POST | /资源 | POST /users |
| 修改 | PUT | /资源/{id} | PUT /users/1 |
| 删除 | DELETE | /资源/{id} | DELETE /users/1 |
### 11.2 响应格式
```json
{
"code": 200,
"message": "操作成功",
"data": {},
"timestamp": 1704067200000
}
```
## 十二、安全规范
1. 敏感配置使用环境变量或配置中心
2. 密码使用 BCrypt 加密存储
3. 接口做权限校验
4. 防止SQL注入使用参数化查询
5. 防止XSS对输出内容转义
6. 日志脱敏处理敏感信息
## 十三、Git提交规范
```
<type>(<scope>): <subject>
feat: 新功能
fix: 修复bug
docs: 文档更新
style: 代码格式
refactor: 重构
test: 测试
chore: 构建/工具
```
示例:
```
feat(user): 添加用户注册功能
fix(order): 修复订单金额计算错误
docs(readme): 更新部署文档
```

166
pom.xml Normal file
View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/>
</parent>
<groupId>com.dora</groupId>
<artifactId>weixin-backend</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>weixin-backend</name>
<description>1818AIGC微信后端服务</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<cos.version>5.6.191</cos.version>
<jjwt.version>0.12.5</jjwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot JDBC (for SQL init) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 腾讯云 COS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${cos.version}</version>
</dependency>
<!-- 腾讯云 VOD SDK -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-vod</artifactId>
<version>3.1.1411</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger/OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Spring Security Crypto (for BCrypt) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.6.0</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.dora;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 微信后端服务启动类
*
* @author dora
*/
@SpringBootApplication
@EnableScheduling
public class WeixinApplication {
public static void main(String[] args) {
SpringApplication.run(WeixinApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package com.dora.annotation;
import java.lang.annotation.*;
/**
* 权限校验注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
/**
* 需要的权限码
*/
String[] value() default {};
/**
* 是否需要全部权限AND逻辑默认falseOR逻辑
*/
boolean requireAll() default false;
}

View File

@@ -0,0 +1,78 @@
package com.dora.aspect;
import com.dora.annotation.RequirePermission;
import com.dora.common.context.AdminContext;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.ResultCode;
import com.dora.mapper.AdminMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
/**
* 权限校验切面
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class PermissionAspect {
private final AdminMapper adminMapper;
@Around("@annotation(com.dora.annotation.RequirePermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequirePermission annotation = method.getAnnotation(RequirePermission.class);
if (annotation == null || annotation.value().length == 0) {
return joinPoint.proceed();
}
// 获取当前管理员ID
Long adminId = AdminContext.getAdminId();
if (adminId == null) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
// 获取管理员权限
List<String> permissions = adminMapper.selectPermissionCodesByAdminId(adminId);
// 超级管理员拥有所有权限
List<String> roles = adminMapper.selectRoleCodesByAdminId(adminId);
if (roles.contains("SUPER_ADMIN")) {
return joinPoint.proceed();
}
// 校验权限
String[] requiredPermissions = annotation.value();
boolean hasPermission;
if (annotation.requireAll()) {
// 需要全部权限
hasPermission = permissions.containsAll(Arrays.asList(requiredPermissions));
} else {
// 只需要其中一个权限
hasPermission = Arrays.stream(requiredPermissions)
.anyMatch(permissions::contains);
}
if (!hasPermission) {
log.warn("权限不足: adminId={}, required={}, has={}",
adminId, Arrays.toString(requiredPermissions), permissions);
throw new BusinessException(ResultCode.FORBIDDEN);
}
return joinPoint.proceed();
}
}

View File

@@ -0,0 +1,31 @@
package com.dora.common.context;
/**
* 管理员上下文
*/
public class AdminContext {
private static final ThreadLocal<Long> ADMIN_ID = new ThreadLocal<>();
private static final ThreadLocal<String> USERNAME = new ThreadLocal<>();
public static void setAdminId(Long adminId) {
ADMIN_ID.set(adminId);
}
public static Long getAdminId() {
return ADMIN_ID.get();
}
public static void setUsername(String username) {
USERNAME.set(username);
}
public static String getUsername() {
return USERNAME.get();
}
public static void clear() {
ADMIN_ID.remove();
USERNAME.remove();
}
}

View File

@@ -0,0 +1,46 @@
package com.dora.common.context;
/**
* 用户上下文,存储当前请求的用户信息
*/
public class UserContext {
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
private static final ThreadLocal<String> OPENID = new ThreadLocal<>();
/**
* 设置用户ID
*/
public static void setUserId(Long userId) {
USER_ID.set(userId);
}
/**
* 获取用户ID
*/
public static Long getUserId() {
return USER_ID.get();
}
/**
* 设置openid
*/
public static void setOpenid(String openid) {
OPENID.set(openid);
}
/**
* 获取openid
*/
public static String getOpenid() {
return OPENID.get();
}
/**
* 清理上下文
*/
public static void clear() {
USER_ID.remove();
OPENID.remove();
}
}

View File

@@ -0,0 +1,35 @@
package com.dora.common.exception;
import com.dora.common.result.ResultCode;
import lombok.Getter;
/**
* 业务异常
*
* @author dora
*/
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = ResultCode.FAIL.getCode();
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
}
}

View File

@@ -0,0 +1,78 @@
package com.dora.common.exception;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*
* @author dora
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError() != null
? e.getBindingResult().getFieldError().getDefaultMessage()
: "参数校验失败";
log.warn("参数校验异常: {}", message);
return Result.fail(ResultCode.PARAM_ERROR.getCode(), message);
}
/**
* 绑定异常
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleBindException(BindException e) {
String message = e.getFieldError() != null
? e.getFieldError().getDefaultMessage()
: "参数绑定失败";
log.warn("参数绑定异常: {}", message);
return Result.fail(ResultCode.PARAM_ERROR.getCode(), message);
}
/**
* 约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
log.warn("约束违反异常: {}", e.getMessage());
return Result.fail(ResultCode.PARAM_ERROR.getCode(), e.getMessage());
}
/**
* 其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e) {
log.error("系统异常: ", e);
return Result.fail(ResultCode.INTERNAL_ERROR);
}
}

View File

@@ -0,0 +1,59 @@
package com.dora.common.result;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结果
*
* @param <T> 数据类型
* @author dora
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private T data;
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
result.setData(data);
return result;
}
public static <T> Result<T> fail(String message) {
Result<T> result = new Result<>();
result.setCode(ResultCode.FAIL.getCode());
result.setMessage(message);
return result;
}
public static <T> Result<T> fail(ResultCode resultCode) {
Result<T> result = new Result<>();
result.setCode(resultCode.getCode());
result.setMessage(resultCode.getMessage());
return result;
}
public static <T> Result<T> fail(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}

View File

@@ -0,0 +1,81 @@
package com.dora.common.result;
import lombok.Getter;
/**
* 响应状态码枚举
*
* @author dora
*/
@Getter
public enum ResultCode {
// ========== 成功 ==========
SUCCESS(0, "操作成功"),
// ========== 通用失败 ==========
FAIL(1, "操作失败"),
// ========== 客户端错误 1xxx ==========
PARAM_ERROR(1001, "参数错误"),
PARAM_MISSING(1002, "参数缺失"),
PARAM_TYPE_ERROR(1003, "参数类型错误"),
PARAM_VALID_ERROR(1004, "参数校验失败"),
// ========== 认证授权 2xxx ==========
UNAUTHORIZED(2001, "未登录或登录已过期"),
TOKEN_INVALID(2002, "Token无效"),
TOKEN_EXPIRED(2003, "Token已过期"),
FORBIDDEN(2004, "无权限访问"),
ACCOUNT_DISABLED(2005, "账号已被禁用"),
ACCOUNT_LOCKED(2006, "账号已被锁定"),
LOGIN_FAILED(2007, "用户名或密码错误"),
// ========== 用户相关 3xxx ==========
USER_NOT_FOUND(3001, "用户不存在"),
USER_ALREADY_EXISTS(3002, "用户已存在"),
USER_PASSWORD_ERROR(3003, "密码错误"),
USER_PHONE_EXISTS(3004, "手机号已被注册"),
USER_EMAIL_EXISTS(3005, "邮箱已被注册"),
// ========== 业务错误 4xxx ==========
DATA_NOT_FOUND(4001, "数据不存在"),
DATA_ALREADY_EXISTS(4002, "数据已存在"),
DATA_SAVE_FAILED(4003, "数据保存失败"),
DATA_UPDATE_FAILED(4004, "数据更新失败"),
DATA_DELETE_FAILED(4005, "数据删除失败"),
OPERATION_FAILED(4006, "操作失败"),
OPERATION_NOT_ALLOWED(4007, "操作不允许"),
OPERATION_TOO_FREQUENT(4008, "操作过于频繁,请稍后再试"),
VERIFICATION_CODE_ERROR(4009, "验证码错误或已过期"),
EMAIL_SEND_FAILED(4010, "邮件发送失败"),
// ========== 微信相关 5xxx ==========
WECHAT_AUTH_FAILED(5001, "微信授权失败"),
WECHAT_CODE_INVALID(5002, "微信code无效"),
WECHAT_USER_NOT_BOUND(5003, "微信用户未绑定"),
WECHAT_API_ERROR(5004, "微信接口调用失败"),
// ========== 文件相关 6xxx ==========
FILE_NOT_FOUND(6001, "文件不存在"),
FILE_UPLOAD_FAILED(6002, "文件上传失败"),
FILE_TYPE_NOT_ALLOWED(6003, "文件类型不允许"),
FILE_SIZE_EXCEEDED(6004, "文件大小超出限制"),
// ========== 系统错误 9xxx ==========
INTERNAL_ERROR(9000, "系统内部错误"),
SYSTEM_ERROR(9001, "系统错误"),
SERVICE_UNAVAILABLE(9002, "服务不可用"),
NETWORK_ERROR(9003, "网络异常"),
DATABASE_ERROR(9004, "数据库异常"),
THIRD_PARTY_ERROR(9005, "第三方服务异常"),
RATE_LIMIT_EXCEEDED(9006, "请求过于频繁");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@@ -0,0 +1,47 @@
package com.dora.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.region.Region;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 腾讯云 COS 配置
*
* @author dora
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "tencent.cos")
public class CosConfig {
private String secretId;
private String secretKey;
private String region;
private String bucketName;
private String userImgFolder;
private Integer expirationSeconds;
private String customDomain;
@Bean
public COSClient cosClient() {
COSCredentials credentials = new BasicCOSCredentials(secretId, secretKey);
ClientConfig clientConfig = new ClientConfig(new Region(region));
return new COSClient(credentials, clientConfig);
}
/**
* 获取文件访问URL
*/
public String getFileUrl(String key) {
if (customDomain != null && !customDomain.isEmpty()) {
return customDomain + "/" + key;
}
return String.format("https://%s.cos.%s.myqcloud.com/%s", bucketName, region, key);
}
}

View File

@@ -0,0 +1,63 @@
package com.dora.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import javax.sql.DataSource;
/**
* 数据库初始化配置
* 每次启动时执行 schema.sql 和 data.sql
*/
@Slf4j
@Configuration
public class DatabaseInitConfig {
@Value("${spring.sql.init.mode:never}")
private String initMode;
@Bean
public CommandLineRunner databaseInitializer(DataSource dataSource) {
return args -> {
if (!"always".equalsIgnoreCase(initMode)) {
log.info("数据库初始化已禁用 (mode={})", initMode);
return;
}
log.info("========== 开始初始化数据库 ==========");
try {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setSqlScriptEncoding("UTF-8");
populator.setSeparator(";");
populator.setContinueOnError(false);
// 执行 schema.sql删除并重建表
ClassPathResource schemaResource = new ClassPathResource("db/schema.sql");
if (schemaResource.exists()) {
log.info("执行 schema.sql ...");
populator.addScript(schemaResource);
}
// 执行 data.sql插入初始数据
ClassPathResource dataResource = new ClassPathResource("db/data.sql");
if (dataResource.exists()) {
log.info("执行 data.sql ...");
populator.addScript(dataResource);
}
populator.execute(dataSource);
log.info("========== 数据库初始化完成 ==========");
} catch (Exception e) {
log.error("数据库初始化失败: {}", e.getMessage(), e);
throw new RuntimeException("数据库初始化失败", e);
}
};
}
}

View File

@@ -0,0 +1,28 @@
package com.dora.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus 配置
*
* @author dora
*/
@Configuration
@MapperScan("com.dora.mapper")
public class MyBatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,46 @@
package com.dora.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis 配置
*
* @author dora
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化配置
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// key 使用 String 序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value 使用 JSON 序列化
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,21 @@
package com.dora.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(30000); // 连接超时30秒
factory.setReadTimeout(120000); // 读取超时120秒AI生成需要较长时间
return new RestTemplate(factory);
}
}

View File

@@ -0,0 +1,63 @@
package com.dora.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* Swagger/OpenAPI 配置
*/
@Configuration
public class SwaggerConfig {
@Value("${swagger.server-url:}")
private String serverUrl;
@Bean
public OpenAPI openAPI() {
OpenAPI openAPI = new OpenAPI()
.info(new Info()
.title("1818AIGC 微信小程序后端 API 文档")
.description("1818AIGC 微信小程序后端接口文档,包含用户认证、作品管理、分类管理等模块")
.version("1.0.0")
.contact(new Contact()
.name("1818AIGC 技术团队")
.email("support@1818aigc.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.components(new Components()
.addSecuritySchemes("Bearer认证", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT令牌认证请在值中填入: Bearer {你的token}")))
.addSecurityItem(new SecurityRequirement().addList("Bearer认证"));
// 配置服务器地址解决HTTPS环境下Swagger请求HTTP的问题
if (serverUrl != null && !serverUrl.isEmpty()) {
openAPI.servers(List.of(
new Server().url(serverUrl).description("生产环境 (HTTPS)"),
new Server().url("http://localhost:8080/api").description("本地开发环境")
));
} else {
// 默认配置,防止未设置时使用错误的协议
openAPI.servers(List.of(
new Server().url("https://api.1818ai.com/api").description("生产环境 (HTTPS)"),
new Server().url("http://localhost:8080/api").description("本地开发环境")
));
}
return openAPI;
}
}

View File

@@ -0,0 +1,155 @@
package com.dora.config;
import com.dora.interceptor.AdminAuthInterceptor;
import com.dora.interceptor.JwtAuthInterceptor;
import com.dora.interceptor.SwaggerAuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.util.unit.DataSize;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.servlet.MultipartConfigElement;
import java.util.Arrays;
/**
* Web配置
*
* @author dora
*/
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtAuthInterceptor jwtAuthInterceptor;
private final SwaggerAuthInterceptor swaggerAuthInterceptor;
private final AdminAuthInterceptor adminAuthInterceptor;
/**
* 配置文件上传解析器
*/
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
/**
* CORS 过滤器 - 优先级最高,确保在拦截器之前处理跨域
*/
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setExposedHeaders(Arrays.asList("*"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
// 设置最高优先级,确保在所有拦截器之前执行
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
/**
* 配置文件上传大小限制
*/
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 设置单个文件最大大小为10MB
factory.setMaxFileSize(DataSize.ofMegabytes(10));
// 设置总上传数据最大大小为10MB
factory.setMaxRequestSize(DataSize.ofMegabytes(10));
return factory.createMultipartConfig();
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// 确保路径匹配不使用后缀模式
configurer.setUseTrailingSlashMatch(false);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Swagger 访问密码拦截器(优先级最高)
registry.addInterceptor(swaggerAuthInterceptor)
.addPathPatterns("/swagger-ui.html", "/swagger-ui/**", "/swagger-ui/index.html")
.excludePathPatterns("/v3/api-docs/**", "/swagger-resources/**", "/webjars/**")
.order(0);
// 管理员认证拦截器
registry.addInterceptor(adminAuthInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns(
"/admin/auth/login",
"/admin/auth/login/email",
"/admin/auth/send-code",
"/admin/auth/register"
)
.order(2);
// JWT 认证拦截器(小程序用户)
registry.addInterceptor(jwtAuthInterceptor)
.addPathPatterns("/**")
// 排除不需要认证的接口
.excludePathPatterns(
// 管理端接口由AdminAuthInterceptor处理
"/admin/**",
// 登录相关
"/user/check",
"/user/wx-login",
"/user/refresh-token",
// 公开接口 - 作品相关(/work/*匹配/work/{id}和/work/list不匹配/work/{id}/like
"/work/*",
// 分类和Banner
"/category/**",
"/banner/**",
// AI模型列表公开
"/ai/models",
"/ai/models/home",
"/ai/tasks/public/**",
"/ai/tasks/no/**", // 支持公开访问已完成的任务
// 积分套餐(公开)和支付回调
"/points/packages",
"/points/notify/**",
// 头像上传(登录流程中使用)
"/user/upload-avatar",
// Swagger文档
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/**",
// 错误页面
"/error"
)
.order(3);
}
}

View File

@@ -0,0 +1,46 @@
package com.dora.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 微信小程序配置
*
* @author dora
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "wechat.miniapp")
public class WechatConfig {
/**
* 小程序AppID
*/
private String appid;
/**
* 小程序密钥
*/
private String secret;
/**
* 获取code2session接口URL
*/
public String getCode2SessionUrl(String code) {
return String.format(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appid, secret, code
);
}
/**
* 获取access_token接口URL
*/
public String getAccessTokenUrl() {
return String.format(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
appid, secret
);
}
}

View File

@@ -0,0 +1,36 @@
package com.dora.config;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "wx.pay")
public class WxPayConfig {
private String appId;
private String mchId;
private String mchKey;
private String tradeType;
private String notifyUrl;
private String certPath;
@Bean
public WxPayService wxPayService() {
com.github.binarywang.wxpay.config.WxPayConfig payConfig =
new com.github.binarywang.wxpay.config.WxPayConfig();
payConfig.setAppId(appId);
payConfig.setMchId(mchId);
payConfig.setMchKey(mchKey);
payConfig.setKeyPath(certPath);
payConfig.setTradeType(tradeType);
payConfig.setNotifyUrl(notifyUrl);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}

View File

@@ -0,0 +1,172 @@
package com.dora.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.dto.AiTaskDTO;
import com.dora.service.AiModelService;
import com.dora.service.AiTaskService;
import com.dora.util.JwtUtil;
import com.dora.vo.AiTaskVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
@Tag(name = "AI功能")
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
@Slf4j
public class AiController {
private final AiModelService aiModelService;
private final AiTaskService aiTaskService;
private final JwtUtil jwtUtil;
@Operation(summary = "获取可用的AI模型列表")
@GetMapping("/models")
public Result<?> getModels(@RequestParam(required = false) String type) {
// 用户端使用简化VO不返回敏感信息
return Result.success(aiModelService.getUserModels(type));
}
@Operation(summary = "获取模型分类列表(用于资产页面筛选)")
@GetMapping("/models/categories")
public Result<?> getModelCategories() {
// 返回启用状态的模型列表,用于资产页面的分类筛选
return Result.success(aiModelService.getUserModels(null));
}
@Operation(summary = "获取首页展示的模型列表")
@GetMapping("/models/home")
public Result<?> getHomeModels(@RequestParam(defaultValue = "4") Integer limit) {
return Result.success(aiModelService.getHomeModels(limit));
}
@Operation(summary = "获取模型详情")
@GetMapping("/models/{id}")
public Result<?> getModel(@PathVariable Long id) {
// 用户端使用简化VO不返回敏感信息
try {
return Result.success(aiModelService.getModelSimpleById(id));
} catch (Exception e) {
return Result.success(null);
}
}
@Operation(summary = "根据编码获取模型详情")
@GetMapping("/models/code/{code}")
public Result<?> getModelByCode(@PathVariable String code) {
// 用户端使用简化VO不返回敏感信息
// 模型不存在时返回null避免前端批量加载模型时弹错误提示
try {
return Result.success(aiModelService.getModelSimpleByCode(code));
} catch (Exception e) {
return Result.success(null);
}
}
@Operation(summary = "创建AI任务")
@PostMapping("/tasks")
public Result<?> createTask(@Valid @RequestBody AiTaskDTO dto, HttpServletRequest request) {
Long userId = jwtUtil.getUserIdFromRequest(request);
String taskNo = aiTaskService.createTask(userId, dto);
return Result.success(taskNo);
}
@Operation(summary = "获取我的任务列表")
@GetMapping("/tasks")
public Result<?> getMyTasks(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long modelId,
HttpServletRequest request) {
Long userId = jwtUtil.getUserIdFromRequest(request);
Page<AiTaskVO> pageParam = new Page<>(page, size);
return Result.success(aiTaskService.getTaskPage(pageParam, userId, status, modelId));
}
@Operation(summary = "获取任务详情")
@GetMapping("/tasks/{id}")
public Result<?> getTask(@PathVariable Long id, HttpServletRequest request) {
Long userId = jwtUtil.getUserIdFromRequest(request);
AiTaskVO task = aiTaskService.getTaskById(id);
// 检查权限
if (!task.getUserId().equals(userId)) {
return Result.fail("无权限查看");
}
return Result.success(task);
}
@Operation(summary = "根据任务编号获取任务详情")
@GetMapping("/tasks/no/{taskNo}")
public Result<?> getTaskByNo(@PathVariable String taskNo, HttpServletRequest request) {
log.info("获取任务详情: taskNo={}", taskNo);
try {
AiTaskVO task = aiTaskService.getTaskByNo(taskNo);
// 尝试获取用户ID可能为null表示未登录用户
Long userId = null;
try {
userId = jwtUtil.getUserIdFromRequest(request);
} catch (Exception e) {
// 用户未登录userId保持为null
log.debug("用户未登录访问任务: taskNo={}", taskNo);
}
// 如果用户已登录且是任务创建者,返回完整信息
if (userId != null && task.getUserId() != null && task.getUserId().equals(userId)) {
log.info("任务创建者访问: taskNo={}, userId={}", taskNo, userId);
return Result.success(task);
}
// 如果是未登录用户或其他用户,检查任务是否可以公开访问
// 只有已完成的任务才能公开访问
if (task.getStatus() != null && task.getStatus() == 2) {
log.info("公开访问已完成任务: taskNo={}", taskNo);
return Result.success(task);
} else {
log.warn("尝试访问未完成的任务: taskNo={}, status={}", taskNo, task.getStatus());
return Result.fail("无权限查看");
}
} catch (BusinessException e) {
log.warn("业务异常: taskNo={}, message={}", taskNo, e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("获取任务详情失败: taskNo={}", taskNo, e);
return Result.fail("系统错误,请稍后重试");
}
}
@Operation(summary = "获取任务详情(公开接口,不需要认证)")
@GetMapping("/tasks/public/{id}")
public Result<?> getTaskPublic(@PathVariable Long id) {
AiTaskVO task = aiTaskService.getTaskById(id);
return Result.success(task);
}
@Operation(summary = "取消任务")
@PostMapping("/tasks/{id}/cancel")
public Result<?> cancelTask(@PathVariable Long id, HttpServletRequest request) {
Long userId = jwtUtil.getUserIdFromRequest(request);
aiTaskService.cancelTask(userId, id);
return Result.success();
}
@Operation(summary = "删除任务")
@DeleteMapping("/tasks/{id}")
public Result<?> deleteTask(@PathVariable Long id, HttpServletRequest request) {
Long userId = jwtUtil.getUserIdFromRequest(request);
aiTaskService.deleteTask(userId, id);
return Result.success();
}
}

View File

@@ -0,0 +1,37 @@
package com.dora.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dora.common.result.Result;
import com.dora.entity.Banner;
import com.dora.mapper.BannerMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
@Tag(name = "Banner模块")
@RestController
@RequestMapping("/banner")
@RequiredArgsConstructor
public class BannerController {
private final BannerMapper bannerMapper;
@Operation(summary = "获取Banner列表")
@GetMapping("/list")
public Result<List<Banner>> getBannerList(@RequestParam(defaultValue = "home") String position) {
LocalDateTime now = LocalDateTime.now();
List<Banner> banners = bannerMapper.selectList(
new LambdaQueryWrapper<Banner>()
.eq(Banner::getStatus, 1)
.eq(Banner::getPosition, position)
.and(w -> w.isNull(Banner::getStartTime).or().le(Banner::getStartTime, now))
.and(w -> w.isNull(Banner::getEndTime).or().ge(Banner::getEndTime, now))
.orderByDesc(Banner::getSort)
);
return Result.success(banners);
}
}

View File

@@ -0,0 +1,32 @@
package com.dora.controller;
import com.dora.common.result.Result;
import com.dora.service.WorkCategoryService;
import com.dora.vo.CategoryVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 分类控制器
*/
@Tag(name = "分类模块", description = "作品分类相关接口公开接口无需Token验证")
@RestController
@RequestMapping("/category")
@RequiredArgsConstructor
public class CategoryController {
private final WorkCategoryService workCategoryService;
@Operation(summary = "获取分类列表", description = "获取所有启用的作品分类")
@GetMapping("/list")
public Result<List<CategoryVO>> list() {
List<CategoryVO> categories = workCategoryService.listCategories();
return Result.success(categories);
}
}

View File

@@ -0,0 +1,27 @@
package com.dora.controller;
import com.dora.common.result.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 健康检查控制器
*
* @author dora
*/
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping
public Result<Map<String, Object>> health() {
Map<String, Object> info = new HashMap<>();
info.put("status", "UP");
info.put("timestamp", System.currentTimeMillis());
return Result.success(info);
}
}

View File

@@ -0,0 +1,225 @@
package com.dora.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import com.dora.entity.Notice;
import com.dora.entity.NoticeRead;
import com.dora.mapper.NoticeMapper;
import com.dora.mapper.NoticeReadMapper;
import com.dora.common.context.UserContext;
import com.dora.vo.PageVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 用户端公告控制器
*/
@Tag(name = "公告模块")
@RestController
@RequestMapping("/notice")
@RequiredArgsConstructor
public class NoticeController {
private final NoticeMapper noticeMapper;
private final NoticeReadMapper noticeReadMapper;
@Operation(summary = "获取公告列表")
@GetMapping("/list")
public Result<PageVO<Notice>> getNoticeList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer type) {
LocalDateTime now = LocalDateTime.now();
Page<Notice> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Notice> wrapper = new LambdaQueryWrapper<Notice>()
.eq(Notice::getStatus, 1)
.and(w -> w.isNull(Notice::getStartTime).or().le(Notice::getStartTime, now))
.and(w -> w.isNull(Notice::getEndTime).or().ge(Notice::getEndTime, now));
if (type != null) {
wrapper.eq(Notice::getType, type);
}
wrapper.orderByDesc(Notice::getIsTop)
.orderByDesc(Notice::getCreatedAt);
Page<Notice> result = noticeMapper.selectPage(pageParam, wrapper);
return Result.success(PageVO.of(result.getRecords(), result.getTotal(), page, pageSize));
}
@Operation(summary = "获取公告详情")
@GetMapping("/{id}")
public Result<Notice> getNoticeDetail(@PathVariable Long id) {
Notice notice = noticeMapper.selectById(id);
if (notice == null || notice.getStatus() != 1) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "公告不存在");
}
// 增加浏览次数
notice.setViewCount(notice.getViewCount() == null ? 1 : notice.getViewCount() + 1);
noticeMapper.updateById(notice);
return Result.success(notice);
}
@Operation(summary = "获取未读公告数量")
@GetMapping("/unread/count")
public Result<Long> getUnreadCount() {
Long userId = UserContext.getUserId();
LocalDateTime now = LocalDateTime.now();
// 获取有效公告ID列表
List<Notice> validNotices = noticeMapper.selectList(
new LambdaQueryWrapper<Notice>()
.select(Notice::getId)
.eq(Notice::getStatus, 1)
.and(w -> w.isNull(Notice::getStartTime).or().le(Notice::getStartTime, now))
.and(w -> w.isNull(Notice::getEndTime).or().ge(Notice::getEndTime, now))
);
if (validNotices.isEmpty()) {
return Result.success(0L);
}
Set<Long> validNoticeIds = validNotices.stream()
.map(Notice::getId)
.collect(Collectors.toSet());
// 获取用户已读公告ID列表
List<NoticeRead> readRecords = noticeReadMapper.selectList(
new LambdaQueryWrapper<NoticeRead>()
.eq(NoticeRead::getUserId, userId)
.in(NoticeRead::getNoticeId, validNoticeIds)
);
Set<Long> readNoticeIds = readRecords.stream()
.map(NoticeRead::getNoticeId)
.collect(Collectors.toSet());
long unreadCount = validNoticeIds.size() - readNoticeIds.size();
return Result.success(Math.max(0, unreadCount));
}
@Operation(summary = "标记公告已读")
@PostMapping("/{id}/read")
public Result<Void> markAsRead(@PathVariable Long id) {
Long userId = UserContext.getUserId();
// 检查公告是否存在
Notice notice = noticeMapper.selectById(id);
if (notice == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "公告不存在");
}
// 检查是否已读
NoticeRead existing = noticeReadMapper.selectOne(
new LambdaQueryWrapper<NoticeRead>()
.eq(NoticeRead::getNoticeId, id)
.eq(NoticeRead::getUserId, userId)
);
if (existing == null) {
NoticeRead readRecord = new NoticeRead();
readRecord.setNoticeId(id);
readRecord.setUserId(userId);
readRecord.setCreatedAt(LocalDateTime.now());
noticeReadMapper.insert(readRecord);
}
return Result.success();
}
@Operation(summary = "标记所有公告已读")
@PostMapping("/read/all")
public Result<Void> markAllAsRead() {
Long userId = UserContext.getUserId();
LocalDateTime now = LocalDateTime.now();
// 获取所有有效公告
List<Notice> validNotices = noticeMapper.selectList(
new LambdaQueryWrapper<Notice>()
.select(Notice::getId)
.eq(Notice::getStatus, 1)
.and(w -> w.isNull(Notice::getStartTime).or().le(Notice::getStartTime, now))
.and(w -> w.isNull(Notice::getEndTime).or().ge(Notice::getEndTime, now))
);
// 获取用户已读公告
List<NoticeRead> readRecords = noticeReadMapper.selectList(
new LambdaQueryWrapper<NoticeRead>()
.eq(NoticeRead::getUserId, userId)
);
Set<Long> readNoticeIds = readRecords.stream()
.map(NoticeRead::getNoticeId)
.collect(Collectors.toSet());
// 标记未读公告为已读
for (Notice notice : validNotices) {
if (!readNoticeIds.contains(notice.getId())) {
NoticeRead readRecord = new NoticeRead();
readRecord.setNoticeId(notice.getId());
readRecord.setUserId(userId);
readRecord.setCreatedAt(LocalDateTime.now());
noticeReadMapper.insert(readRecord);
}
}
return Result.success();
}
@Operation(summary = "获取弹窗公告")
@GetMapping("/popup")
public Result<List<Notice>> getPopupNotices() {
Long userId = UserContext.getUserId();
LocalDateTime now = LocalDateTime.now();
// 获取需要弹窗且未读的公告
List<Notice> popupNotices = noticeMapper.selectList(
new LambdaQueryWrapper<Notice>()
.eq(Notice::getStatus, 1)
.eq(Notice::getIsPopup, 1)
.and(w -> w.isNull(Notice::getStartTime).or().le(Notice::getStartTime, now))
.and(w -> w.isNull(Notice::getEndTime).or().ge(Notice::getEndTime, now))
.orderByDesc(Notice::getLevel)
.orderByDesc(Notice::getCreatedAt)
);
if (popupNotices.isEmpty()) {
return Result.success(popupNotices);
}
// 过滤已读的公告
Set<Long> noticeIds = popupNotices.stream()
.map(Notice::getId)
.collect(Collectors.toSet());
List<NoticeRead> readRecords = noticeReadMapper.selectList(
new LambdaQueryWrapper<NoticeRead>()
.eq(NoticeRead::getUserId, userId)
.in(NoticeRead::getNoticeId, noticeIds)
);
Set<Long> readNoticeIds = readRecords.stream()
.map(NoticeRead::getNoticeId)
.collect(Collectors.toSet());
List<Notice> unreadPopupNotices = popupNotices.stream()
.filter(n -> !readNoticeIds.contains(n.getId()))
.collect(Collectors.toList());
return Result.success(unreadPopupNotices);
}
}

View File

@@ -0,0 +1,53 @@
package com.dora.controller;
import com.dora.common.result.Result;
import com.dora.dto.CreatePointsOrderDTO;
import com.dora.service.PointsService;
import com.dora.vo.PointsPackageVO;
import com.dora.vo.WxPayOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "积分模块")
@RestController
@RequestMapping("/points")
@RequiredArgsConstructor
public class PointsController {
private final PointsService pointsService;
@Operation(summary = "获取积分套餐列表")
@GetMapping("/packages")
public Result<List<PointsPackageVO>> getPackages() {
return Result.success(pointsService.getPackageList());
}
@Operation(summary = "创建积分订单")
@PostMapping("/order")
public Result<WxPayOrderVO> createOrder(
@RequestAttribute("userId") Long userId,
@Valid @RequestBody CreatePointsOrderDTO dto) {
return Result.success(pointsService.createOrder(userId, dto));
}
@Operation(summary = "取消订单")
@PostMapping("/order/cancel")
public Result<Void> cancelOrder(
@RequestAttribute("userId") Long userId,
@RequestParam String orderNo) {
pointsService.cancelOrder(userId, orderNo);
return Result.success();
}
@Operation(summary = "微信支付回调")
@PostMapping("/notify/wechat")
public String wechatNotify(HttpServletRequest request) {
return pointsService.handlePayNotify(request);
}
}

View File

@@ -0,0 +1,163 @@
package com.dora.controller;
import com.dora.common.result.Result;
import com.dora.config.CosConfig;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Slf4j
@Tag(name = "文件上传")
@RestController
@RequestMapping("/upload")
@RequiredArgsConstructor
public class UploadController {
private final COSClient cosClient;
private final CosConfig cosConfig;
@Operation(summary = "上传单个图片")
@PostMapping("/image")
public Result<String> uploadImage(@RequestParam("file") MultipartFile file) {
try {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return Result.fail("只能上传图片文件");
}
// 验证文件大小10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.fail("图片大小不能超过10MB");
}
String url = uploadToCos(file);
return Result.success(url);
} catch (Exception e) {
log.error("上传图片失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
@Operation(summary = "上传多个图片")
@PostMapping("/images")
public Result<List<String>> uploadImages(@RequestParam("files") MultipartFile[] files) {
try {
List<String> urls = new ArrayList<>();
for (MultipartFile file : files) {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return Result.fail("只能上传图片文件");
}
// 验证文件大小10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.fail("图片大小不能超过10MB");
}
String url = uploadToCos(file);
urls.add(url);
}
return Result.success(urls);
} catch (Exception e) {
log.error("上传图片失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
@Operation(summary = "上传视频")
@PostMapping("/video")
public Result<String> uploadVideo(@RequestParam("file") MultipartFile file) {
try {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("video/")) {
return Result.fail("只能上传视频文件");
}
// 验证文件大小100MB
if (file.getSize() > 100 * 1024 * 1024) {
return Result.fail("视频大小不能超过100MB");
}
String url = uploadVideoToCos(file);
return Result.success(url);
} catch (Exception e) {
log.error("上传视频失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
private String uploadToCos(MultipartFile file) throws IOException {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String fileName = "user-upload/" + UUID.randomUUID().toString() + extension;
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 上传到COS
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
fileName,
file.getInputStream(),
metadata
);
cosClient.putObject(putObjectRequest);
// 返回文件URL
return cosConfig.getFileUrl(fileName);
}
private String uploadVideoToCos(MultipartFile file) throws IOException {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = ".mp4";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String fileName = "user-video/" + UUID.randomUUID().toString() + extension;
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 上传到COS
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
fileName,
file.getInputStream(),
metadata
);
cosClient.putObject(putObjectRequest);
// 返回文件URL
return cosConfig.getFileUrl(fileName);
}
}

View File

@@ -0,0 +1,261 @@
package com.dora.controller;
import com.dora.common.context.UserContext;
import com.dora.common.result.Result;
import com.dora.dto.RefreshTokenDTO;
import com.dora.dto.UpdateProfileDTO;
import com.dora.dto.WxLoginDTO;
import com.dora.service.AiWorkService;
import com.dora.service.UserService;
import com.dora.vo.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* 用户控制器
*/
@Tag(name = "用户模块", description = "用户登录、注册、信息管理相关接口")
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
private final AiWorkService aiWorkService;
@Operation(summary = "检查用户状态", description = "检查用户是否存在及信息完整度,用于登录前判断")
@PostMapping("/check")
public Result<UserCheckVO> checkUser(@RequestBody Map<String, String> params) {
String code = params.get("code");
if (code == null || code.isEmpty()) {
return Result.fail(4001, "code不能为空");
}
UserCheckVO result = userService.checkUser(code);
return Result.success(result);
}
@Operation(summary = "微信登录", description = "微信小程序授权登录,支持新用户注册和老用户登录")
@PostMapping("/wx-login")
public Result<LoginVO> wxLogin(@RequestBody WxLoginDTO dto) {
LoginVO loginVO = userService.wxLogin(dto);
return Result.success(loginVO);
}
@Operation(summary = "刷新Token", description = "使用refreshToken刷新accessToken避免用户重新登录")
@PostMapping("/refresh-token")
public Result<TokenVO> refreshToken(@RequestBody RefreshTokenDTO dto) {
if (dto.getRefreshToken() == null || dto.getRefreshToken().isEmpty()) {
return Result.fail(4001, "refreshToken不能为空");
}
try {
TokenVO tokenVO = userService.refreshToken(dto.getRefreshToken());
return Result.success(tokenVO);
} catch (RuntimeException e) {
return Result.fail(2001, e.getMessage());
}
}
@Operation(summary = "上传头像", description = "将微信临时头像持久化到COS存储")
@PostMapping("/upload-avatar")
public Result<String> uploadAvatar(
@Parameter(description = "头像文件") @RequestParam("file") MultipartFile file) {
// 获取当前用户信息用于日志
String currentUserInfo = "未知用户";
try {
Long userId = com.dora.common.context.UserContext.getUserId();
if (userId != null) {
currentUserInfo = "用户ID:" + userId;
}
} catch (Exception e) {
log.warn("获取当前用户ID失败", e);
}
// 记录请求信息
log.info("=== 头像上传请求开始 ===");
log.info("当前用户: {}", currentUserInfo);
log.info("文件名: {}", file.getOriginalFilename());
log.info("文件大小: {} bytes", file.getSize());
log.info("文件类型: {}", file.getContentType());
log.info("是否为空: {}", file.isEmpty());
if (file.isEmpty()) {
log.warn("头像上传失败: 文件为空");
return Result.fail(4001, "请选择要上传的文件");
}
// 检查文件大小限制10MB
if (file.getSize() > 10 * 1024 * 1024) {
log.warn("头像上传失败: 文件过大,大小: {} bytes", file.getSize());
return Result.fail(4002, "文件大小不能超过10MB");
}
// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || (!contentType.startsWith("image/"))) {
log.warn("头像上传失败: 文件类型不支持,类型: {}", contentType);
return Result.fail(4003, "只支持图片文件");
}
try {
String avatarUrl = userService.uploadAvatar(file);
log.info("=== 头像上传请求成功 ===");
log.info("用户: {}", currentUserInfo);
log.info("返回URL: {}", avatarUrl);
return Result.success(avatarUrl);
} catch (Exception e) {
log.error("=== 头像上传请求失败 ===");
log.error("用户: {}", currentUserInfo);
log.error("异常详情:", e);
return Result.fail(5001, "头像上传失败: " + e.getMessage());
}
}
@Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息")
@GetMapping("/info")
public Result<LoginVO> getUserInfo() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
LoginVO userInfo = userService.getUserInfo(userId);
if (userInfo == null) {
return Result.fail(2002, "用户不存在");
}
return Result.success(userInfo);
}
@Operation(summary = "获取用户主页信息", description = "获取用户主页展示的信息,包含统计数据")
@GetMapping("/profile")
public Result<UserProfileVO> getUserProfile() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
UserProfileVO profile = userService.getUserProfile(userId);
if (profile == null) {
return Result.fail(2002, "用户不存在");
}
return Result.success(profile);
}
@Operation(summary = "获取用户发布的作品", description = "分页获取当前用户发布的作品列表")
@GetMapping("/works")
public Result<PageVO<WorkVO>> getUserWorks(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
PageVO<WorkVO> page = aiWorkService.getUserWorks(userId, pageNum, pageSize);
return Result.success(page);
}
@Operation(summary = "获取用户点赞的作品", description = "分页获取当前用户点赞的作品列表")
@GetMapping("/liked-works")
public Result<PageVO<WorkVO>> getUserLikedWorks(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
PageVO<WorkVO> page = aiWorkService.getUserLikedWorks(userId, pageNum, pageSize);
return Result.success(page);
}
@Operation(summary = "更新用户资料", description = "更新当前用户的昵称和头像")
@PostMapping("/update-profile")
public Result<Void> updateProfile(@RequestBody UpdateProfileDTO dto) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
userService.updateProfile(userId, dto.getNickname(), dto.getAvatar());
return Result.success();
}
@Operation(summary = "获取订阅状态", description = "获取当前用户是否已订阅消息通知")
@GetMapping("/subscribed")
public Result<Boolean> isSubscribed() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
boolean subscribed = userService.isSubscribed(userId);
return Result.success(subscribed);
}
@Operation(summary = "更新订阅状态", description = "更新当前用户的订阅消息通知状态")
@PostMapping("/subscribed")
public Result<Void> updateSubscribed(@RequestBody Map<String, Boolean> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
Boolean subscribed = params.get("subscribed");
if (subscribed == null) {
return Result.fail(4001, "参数错误");
}
userService.updateSubscribed(userId, subscribed);
return Result.success();
}
@Operation(summary = "获取邀请统计", description = "获取当前用户的邀请统计信息")
@GetMapping("/invite-stats")
public Result<InviteStatsVO> getInviteStats() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
InviteStatsVO stats = userService.getInviteStats(userId);
return Result.success(stats);
}
@Operation(summary = "获取积分统计", description = "获取当前用户的积分统计信息(订阅积分和赠送积分)")
@GetMapping("/points-stats")
public Result<PointsStatsVO> getPointsStats() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
PointsStatsVO stats = userService.getPointsStats(userId);
return Result.success(stats);
}
@Operation(summary = "获取邀请记录", description = "分页获取当前用户的邀请记录列表")
@GetMapping("/invite-records")
public Result<PageVO<InviteRecordVO>> getInviteRecords(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
PageVO<InviteRecordVO> page = userService.getInviteRecords(userId, pageNum, pageSize);
return Result.success(page);
}
@Operation(summary = "获取积分记录", description = "分页获取当前用户的积分记录列表")
@GetMapping("/points-records")
public Result<PageVO<PointsRecordVO>> getPointsRecords(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
@Parameter(description = "类型筛选: 1充值 2消费 3赠送 4推广奖励 5签到 6退款不传则查询全部") @RequestParam(required = false) Integer type) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(2001, "请先登录");
}
PageVO<PointsRecordVO> page = userService.getPointsRecords(userId, pageNum, pageSize, type);
return Result.success(page);
}
}

View File

@@ -0,0 +1,265 @@
package com.dora.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.common.result.Result;
import com.dora.dto.video.*;
import com.dora.service.VideoProjectService;
import com.dora.common.context.UserContext;
import com.dora.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/video-project")
@RequiredArgsConstructor
public class VideoProjectController {
private final VideoProjectService projectService;
/**
* 创建项目
*/
@PostMapping("/create")
public Result<Long> createProject() {
Long userId = UserContext.getUserId();
return Result.success(projectService.createProject(userId));
}
/**
* 获取项目详情
*/
@GetMapping("/{projectId}")
public Result<VideoProjectVO> getProject(@PathVariable Long projectId) {
return Result.success(projectService.getProjectById(projectId));
}
/**
* 获取项目列表
*/
@GetMapping("/list")
public Result<IPage<VideoProjectVO>> getProjectList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Integer status) {
Long userId = UserContext.getUserId();
Page<VideoProjectVO> pageParam = new Page<>(page, size);
return Result.success(projectService.getProjectList(pageParam, userId, status));
}
/**
* 更新项目设置
*/
@PutMapping("/{projectId}/settings")
public Result<Void> updateSettings(@PathVariable Long projectId,
@RequestBody VideoProjectDTO dto) {
projectService.updateProjectSettings(projectId, dto);
return Result.success();
}
/**
* 删除项目
*/
@DeleteMapping("/{projectId}")
public Result<Void> deleteProject(@PathVariable Long projectId) {
Long userId = UserContext.getUserId();
projectService.deleteProject(userId, projectId);
return Result.success();
}
/**
* 生成剧本
*/
@PostMapping("/{projectId}/generate-script")
public Result<ScriptGenerationResult> generateScript(
@PathVariable Long projectId,
@RequestBody @Valid GenerateScriptDTO dto) {
Long userId = UserContext.getUserId();
return Result.success(projectService.generateScript(projectId, dto.getIdea(), userId));
}
// ========== 角色管理 ==========
/**
* 获取项目角色列表
*/
@GetMapping("/{projectId}/characters")
public Result<List<ProjectCharacterVO>> getCharacters(@PathVariable Long projectId) {
return Result.success(projectService.getProjectCharacters(projectId));
}
/**
* 保存角色
*/
@PostMapping("/{projectId}/character")
public Result<Long> saveCharacter(@PathVariable Long projectId,
@RequestBody ProjectCharacterDTO dto) {
return Result.success(projectService.saveCharacter(projectId, dto));
}
/**
* 删除角色
*/
@DeleteMapping("/{projectId}/character/{characterId}")
public Result<Void> deleteCharacter(@PathVariable Long projectId,
@PathVariable Long characterId) {
projectService.deleteCharacter(projectId, characterId);
return Result.success();
}
/**
* 生成角色形象
*/
@PostMapping("/{projectId}/character/{characterId}/generate-image")
public Result<String> generateCharacterImage(@PathVariable Long projectId,
@PathVariable Long characterId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.generateCharacterImage(projectId, characterId, userId));
}
/**
* 获取用户角色库(跨项目,已生成形象的角色)
*/
@GetMapping("/character-library")
public Result<List<ProjectCharacterVO>> getUserCharacterLibrary() {
Long userId = UserContext.getUserId();
return Result.success(projectService.getUserCharacterLibrary(userId));
}
/**
* 获取角色模板库
*/
@GetMapping("/character-templates")
public Result<List<CharacterTemplateVO>> getCharacterTemplates(
@RequestParam(required = false) String category) {
return Result.success(projectService.getCharacterTemplates(category));
}
// ========== 场次管理 ==========
/**
* 获取场次列表
*/
@GetMapping("/{projectId}/scenes")
public Result<List<ProjectSceneVO>> getScenes(@PathVariable Long projectId) {
return Result.success(projectService.getProjectScenes(projectId));
}
/**
* 保存场次
*/
@PostMapping("/{projectId}/scene")
public Result<Long> saveScene(@PathVariable Long projectId,
@RequestBody ProjectSceneDTO dto) {
return Result.success(projectService.saveScene(projectId, dto));
}
/**
* 删除场次
*/
@DeleteMapping("/{projectId}/scene/{sceneId}")
public Result<Void> deleteScene(@PathVariable Long projectId,
@PathVariable Long sceneId) {
projectService.deleteScene(projectId, sceneId);
return Result.success();
}
// ========== 分镜管理 ==========
/**
* 获取分镜列表
*/
@GetMapping("/{projectId}/scene/{sceneId}/storyboards")
public Result<List<SceneStoryboardVO>> getStoryboards(@PathVariable Long projectId,
@PathVariable Long sceneId) {
return Result.success(projectService.getSceneStoryboards(sceneId));
}
/**
* 生成分镜
*/
@PostMapping("/{projectId}/scene/{sceneId}/generate-storyboards")
public Result<StoryboardGenerationResult> generateStoryboards(
@PathVariable Long projectId,
@PathVariable Long sceneId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.generateStoryboards(projectId, sceneId, userId));
}
/**
* 更新分镜
*/
@PutMapping("/{projectId}/storyboard/{storyboardId}")
public Result<Void> updateStoryboard(@PathVariable Long projectId,
@PathVariable Long storyboardId,
@RequestBody SceneStoryboardDTO dto) {
projectService.updateStoryboard(projectId, storyboardId, dto);
return Result.success();
}
/**
* 删除分镜
*/
@DeleteMapping("/{projectId}/storyboard/{storyboardId}")
public Result<Void> deleteStoryboard(@PathVariable Long projectId,
@PathVariable Long storyboardId) {
projectService.deleteStoryboard(projectId, storyboardId);
return Result.success();
}
/**
* 新增分镜
*/
@PostMapping("/{projectId}/scene/{sceneId}/storyboard")
public Result<Long> addStoryboard(@PathVariable Long projectId,
@PathVariable Long sceneId,
@RequestParam(required = false) Integer afterIndex) {
return Result.success(projectService.addStoryboard(projectId, sceneId, afterIndex));
}
/**
* 生成分镜画面
*/
@PostMapping("/{projectId}/storyboard/{storyboardId}/generate-image")
public Result<String> generateStoryboardImage(@PathVariable Long projectId,
@PathVariable Long storyboardId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.generateStoryboardImage(projectId, storyboardId, userId));
}
/**
* 智能优化画面描述
*/
@PostMapping("/{projectId}/storyboard/{storyboardId}/optimize-description")
public Result<String> optimizeDescription(@PathVariable Long projectId,
@PathVariable Long storyboardId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.optimizeDescription(projectId, storyboardId, userId));
}
// ========== 视频生成 ==========
/**
* 生成场次视频
* 流程:拼接分镜图 -> 上传COS -> 调用Grok生成10秒视频
*/
@PostMapping("/{projectId}/scene/{sceneId}/generate-video")
public Result<Map<String, Object>> generateSceneVideo(@PathVariable Long projectId,
@PathVariable Long sceneId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.generateSceneVideo(projectId, sceneId, userId));
}
/**
* 合成最终视频(将所有场次视频拼接)
*/
@PostMapping("/{projectId}/composite-final-video")
public Result<Map<String, Object>> compositeFinalVideo(@PathVariable Long projectId) {
Long userId = UserContext.getUserId();
return Result.success(projectService.compositeFinalVideo(projectId, userId));
}
}

View File

@@ -0,0 +1,70 @@
package com.dora.controller;
import com.dora.common.context.UserContext;
import com.dora.common.result.Result;
import com.dora.dto.PublishWorkDTO;
import com.dora.dto.WorkQueryDTO;
import com.dora.service.AiWorkService;
import com.dora.vo.LikeResultVO;
import com.dora.vo.PageVO;
import com.dora.vo.WorkDetailVO;
import com.dora.vo.WorkVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
/**
* 作品控制器
*/
@Tag(name = "作品模块", description = "AI作品广场相关接口包含作品列表、详情、点赞等功能")
@RestController
@RequestMapping("/work")
@RequiredArgsConstructor
public class WorkController {
private final AiWorkService aiWorkService;
@Operation(summary = "获取作品列表", description = "分页获取公开的AI作品列表支持分类筛选")
@GetMapping("/list")
public Result<PageVO<WorkVO>> list(WorkQueryDTO query) {
Long currentUserId = UserContext.getUserId();
PageVO<WorkVO> page = aiWorkService.listWorks(query, currentUserId);
return Result.success(page);
}
@Operation(summary = "获取作品详情", description = "根据作品ID获取作品详细信息")
@GetMapping("/{id}")
public Result<WorkDetailVO> detail(
@Parameter(description = "作品ID") @PathVariable("id") Long id) {
Long currentUserId = UserContext.getUserId();
WorkDetailVO detail = aiWorkService.getWorkDetail(id, currentUserId);
if (detail == null) {
return Result.fail(4004, "作品不存在");
}
return Result.success(detail);
}
@Operation(summary = "点赞/取消点赞", description = "切换作品的点赞状态,已点赞则取消,未点赞则点赞")
@PostMapping("/{id}/like")
public Result<LikeResultVO> toggleLike(
@Parameter(description = "作品ID") @PathVariable("id") Long workId) {
Long userId = UserContext.getUserId();
LikeResultVO result = aiWorkService.toggleLike(workId, userId);
return Result.success(result);
}
@Operation(summary = "发布作品", description = "将AI任务结果发布到广场需要管理员审核")
@PostMapping("/publish")
public Result<Void> publishWork(@Valid @RequestBody PublishWorkDTO dto) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.fail(401, "请先登录");
}
aiWorkService.publishWork(dto, userId);
return Result.success();
}
}

View File

@@ -0,0 +1,263 @@
package com.dora.controller.admin;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.common.result.Result;
import com.dora.config.CosConfig;
import com.dora.dto.AiModelDTO;
import com.dora.dto.AiProviderDTO;
import com.dora.service.AiModelService;
import com.dora.service.AiProviderService;
import com.dora.service.AiTaskService;
import com.dora.vo.AiModelVO;
import com.dora.vo.AiProviderVO;
import com.dora.vo.AiTaskVO;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.Valid;
import java.util.UUID;
@Slf4j
@Tag(name = "管理端-AI管理")
@RestController
@RequestMapping("/admin/ai")
@RequiredArgsConstructor
public class AdminAiController {
private final AiProviderService aiProviderService;
private final AiModelService aiModelService;
private final AiTaskService aiTaskService;
private final COSClient cosClient;
private final CosConfig cosConfig;
// ==================== AI厂商管理 ====================
@Operation(summary = "分页查询AI厂商")
@GetMapping("/providers")
public Result<?> getProviders(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) Integer status) {
Page<AiProviderVO> pageParam = new Page<>(page, size);
return Result.success(aiProviderService.getProviderPage(pageParam, name, status));
}
@Operation(summary = "获取所有启用的厂商")
@GetMapping("/providers/active")
public Result<?> getActiveProviders() {
return Result.success(aiProviderService.getActiveProviders());
}
@Operation(summary = "获取厂商详情")
@GetMapping("/providers/{id}")
public Result<?> getProvider(@PathVariable Long id) {
return Result.success(aiProviderService.getProviderById(id));
}
@Operation(summary = "创建AI厂商")
@PostMapping("/providers")
public Result<?> createProvider(@Valid @RequestBody AiProviderDTO dto) {
aiProviderService.createProvider(dto);
return Result.success();
}
@Operation(summary = "更新AI厂商")
@PutMapping("/providers")
public Result<?> updateProvider(@Valid @RequestBody AiProviderDTO dto) {
aiProviderService.updateProvider(dto);
return Result.success();
}
@Operation(summary = "删除AI厂商")
@DeleteMapping("/providers/{id}")
public Result<?> deleteProvider(@PathVariable Long id) {
aiProviderService.deleteProvider(id);
return Result.success();
}
@Operation(summary = "更新厂商状态")
@PutMapping("/providers/{id}/status")
public Result<?> updateProviderStatus(@PathVariable Long id, @RequestParam Integer status) {
aiProviderService.updateProviderStatus(id, status);
return Result.success();
}
// ==================== AI模型管理 ====================
@Operation(summary = "分页查询AI模型")
@GetMapping("/models")
public Result<?> getModels(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String type,
@RequestParam(required = false) String category,
@RequestParam(required = false) Integer status) {
Page<AiModelVO> pageParam = new Page<>(page, size);
return Result.success(aiModelService.getModelPage(pageParam, type, category, status));
}
@Operation(summary = "获取启用的模型列表")
@GetMapping("/models/active")
public Result<?> getActiveModels(@RequestParam(required = false) String type) {
return Result.success(aiModelService.getActiveModels(type));
}
@Operation(summary = "获取模型详情")
@GetMapping("/models/{id}")
public Result<?> getModel(@PathVariable Long id) {
return Result.success(aiModelService.getModelById(id));
}
@Operation(summary = "创建AI模型")
@PostMapping("/models")
public Result<?> createModel(@Valid @RequestBody AiModelDTO dto) {
aiModelService.createModel(dto);
return Result.success();
}
@Operation(summary = "更新AI模型")
@PutMapping("/models")
public Result<?> updateModel(@Valid @RequestBody AiModelDTO dto) {
aiModelService.updateModel(dto);
return Result.success();
}
@Operation(summary = "删除AI模型")
@DeleteMapping("/models/{id}")
public Result<?> deleteModel(@PathVariable Long id) {
aiModelService.deleteModel(id);
return Result.success();
}
@Operation(summary = "更新模型状态")
@PutMapping("/models/{id}/status")
public Result<?> updateModelStatus(@PathVariable Long id, @RequestParam Integer status) {
aiModelService.updateModelStatus(id, status);
return Result.success();
}
@Operation(summary = "上传模型图片")
@PostMapping("/models/upload")
public Result<?> uploadModelImage(@RequestParam("file") MultipartFile file) {
try {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return Result.fail("只能上传图片文件");
}
// 验证文件大小10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.fail("图片大小不能超过10MB");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String fileName = "model/" + UUID.randomUUID().toString() + extension;
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 上传到COS
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
fileName,
file.getInputStream(),
metadata
);
cosClient.putObject(putObjectRequest);
// 返回文件URL
String url = cosConfig.getFileUrl(fileName);
return Result.success(url);
} catch (Exception e) {
log.error("上传模型图片失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
@Operation(summary = "获取AI模型类型列表")
@GetMapping("/models/types")
public Result<?> getModelTypes() {
java.util.List<java.util.Map<String, String>> types = new java.util.ArrayList<>();
// 文本生成类
types.add(java.util.Map.of("value", "text_generation", "label", "文本生成", "category", "文本"));
// 图片生成类
types.add(java.util.Map.of("value", "image_generation", "label", "图片生成", "category", "图片"));
types.add(java.util.Map.of("value", "text2img", "label", "文生图", "category", "图片"));
types.add(java.util.Map.of("value", "img2img", "label", "图生图", "category", "图片"));
types.add(java.util.Map.of("value", "img_enhance", "label", "图像增强", "category", "图片"));
types.add(java.util.Map.of("value", "img_upscale", "label", "图像放大", "category", "图片"));
types.add(java.util.Map.of("value", "img_colorize", "label", "图像上色", "category", "图片"));
types.add(java.util.Map.of("value", "img_remove_bg", "label", "背景移除", "category", "图片"));
// 视频生成类
types.add(java.util.Map.of("value", "video_generation", "label", "视频生成", "category", "视频"));
types.add(java.util.Map.of("value", "text2video", "label", "文生视频", "category", "视频"));
types.add(java.util.Map.of("value", "img2video", "label", "图生视频", "category", "视频"));
types.add(java.util.Map.of("value", "video_enhance", "label", "视频增强", "category", "视频"));
// 音频类
types.add(java.util.Map.of("value", "text2audio", "label", "文本转语音", "category", "音频"));
types.add(java.util.Map.of("value", "audio2text", "label", "语音转文本", "category", "音频"));
types.add(java.util.Map.of("value", "voice_clone", "label", "声音克隆", "category", "音频"));
types.add(java.util.Map.of("value", "music_gen", "label", "音乐生成", "category", "音频"));
// 其他类
types.add(java.util.Map.of("value", "face_swap", "label", "换脸", "category", "其他"));
types.add(java.util.Map.of("value", "code_gen", "label", "代码生成", "category", "其他"));
types.add(java.util.Map.of("value", "other", "label", "其他", "category", "其他"));
return Result.success(types);
}
// ==================== AI任务管理 ====================
@Operation(summary = "分页查询AI任务")
@GetMapping("/tasks")
public Result<?> getTasks(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Long userId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long modelId) {
Page<AiTaskVO> pageParam = new Page<>(page, size);
return Result.success(aiTaskService.getTaskPage(pageParam, userId, status, modelId));
}
@Operation(summary = "获取任务详情")
@GetMapping("/tasks/{id}")
public Result<?> getTask(@PathVariable Long id) {
return Result.success(aiTaskService.getTaskById(id));
}
@Operation(summary = "处理任务队列")
@PostMapping("/tasks/process")
public Result<?> processTaskQueue() {
aiTaskService.processTaskQueue();
return Result.success();
}
// ==================== AI模型调试 ====================
@Operation(summary = "调试AI模型")
@PostMapping("/models/{id}/debug")
public Result<?> debugModel(@PathVariable Long id, @RequestBody java.util.Map<String, Object> params) {
return Result.success(aiModelService.debugModel(id, params));
}
}

View File

@@ -0,0 +1,90 @@
package com.dora.controller.admin;
import com.dora.common.context.AdminContext;
import com.dora.common.result.Result;
import com.dora.dto.admin.AdminLoginDTO;
import com.dora.dto.admin.AdminRegisterDTO;
import com.dora.dto.admin.EmailCodeDTO;
import com.dora.dto.admin.EmailLoginDTO;
import com.dora.service.AdminService;
import com.dora.service.EmailService;
import com.dora.vo.admin.AdminInfoVO;
import com.dora.vo.admin.AdminLoginVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 管理员认证控制器
*/
@Tag(name = "管理员认证", description = "管理员登录、注册、获取信息")
@RestController
@RequestMapping("/admin/auth")
@RequiredArgsConstructor
public class AdminAuthController {
private final AdminService adminService;
private final EmailService emailService;
@Operation(summary = "管理员登录(用户名密码)")
@PostMapping("/login")
public Result<AdminLoginVO> login(@Valid @RequestBody AdminLoginDTO dto, HttpServletRequest request) {
String ip = getClientIp(request);
return Result.success(adminService.login(dto, ip));
}
@Operation(summary = "发送邮箱验证码")
@PostMapping("/send-code")
public Result<Void> sendEmailCode(@Valid @RequestBody EmailCodeDTO dto) {
// 先校验邮箱是否存在
adminService.checkEmailExists(dto.getEmail());
// 发送验证码
emailService.sendVerificationCode(dto.getEmail());
return Result.success();
}
@Operation(summary = "管理员邮箱验证码登录")
@PostMapping("/login/email")
public Result<AdminLoginVO> loginByEmail(@Valid @RequestBody EmailLoginDTO dto, HttpServletRequest request) {
String ip = getClientIp(request);
return Result.success(adminService.loginByEmail(dto, ip));
}
@Operation(summary = "管理员注册(仅用于测试,无需认证)")
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody AdminRegisterDTO dto) {
adminService.register(dto);
return Result.success();
}
@Operation(summary = "获取当前管理员信息")
@GetMapping("/info")
public Result<AdminInfoVO> getInfo() {
Long adminId = AdminContext.getAdminId();
return Result.success(adminService.getAdminInfo(adminId));
}
@Operation(summary = "退出登录")
@PostMapping("/logout")
public Result<Void> logout() {
// 客户端清除Token即可服务端可以加入黑名单机制
return Result.success();
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@@ -0,0 +1,99 @@
package com.dora.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import com.dora.entity.WorkCategory;
import com.dora.mapper.WorkCategoryMapper;
import com.dora.service.impl.WorkCategoryServiceImpl;
import com.dora.vo.CategoryVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 管理后台分类控制器
*/
@Tag(name = "分类管理", description = "作品分类的查询、新增、修改、删除")
@RestController
@RequestMapping("/admin/category")
@RequiredArgsConstructor
public class AdminCategoryController {
private final WorkCategoryServiceImpl workCategoryService;
private final WorkCategoryMapper workCategoryMapper;
@Operation(summary = "获取分类树(仅启用)")
@GetMapping("/tree")
public Result<List<CategoryVO>> getCategoryTree() {
return Result.success(workCategoryService.listCategories());
}
@Operation(summary = "获取分类列表(全部)")
@GetMapping("/list")
public Result<List<CategoryVO>> getCategoryList() {
return Result.success(workCategoryService.listAllCategories());
}
@Operation(summary = "创建分类")
@PostMapping
public Result<Void> createCategory(@RequestBody CategoryDTO dto) {
WorkCategory category = new WorkCategory();
category.setParentId(dto.getParentId() != null ? dto.getParentId() : 0L);
category.setName(dto.getName());
category.setIcon(dto.getIcon());
category.setSort(dto.getSort() != null ? dto.getSort() : 0);
category.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
category.setCreatedAt(LocalDateTime.now());
category.setUpdatedAt(LocalDateTime.now());
category.setDeleted(0);
workCategoryMapper.insert(category);
return Result.success();
}
@Operation(summary = "更新分类")
@PutMapping("/{id}")
public Result<Void> updateCategory(@PathVariable Long id, @RequestBody CategoryDTO dto) {
WorkCategory category = workCategoryMapper.selectById(id);
if (category == null || category.getDeleted() == 1) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "分类不存在");
}
if (dto.getName() != null) category.setName(dto.getName());
if (dto.getIcon() != null) category.setIcon(dto.getIcon());
if (dto.getSort() != null) category.setSort(dto.getSort());
if (dto.getStatus() != null) category.setStatus(dto.getStatus());
if (dto.getParentId() != null) category.setParentId(dto.getParentId());
category.setUpdatedAt(LocalDateTime.now());
workCategoryMapper.updateById(category);
return Result.success();
}
@Operation(summary = "删除分类")
@DeleteMapping("/{id}")
public Result<Void> deleteCategory(@PathVariable Long id) {
WorkCategory category = workCategoryMapper.selectById(id);
if (category == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "分类不存在");
}
// 逻辑删除
category.setDeleted(1);
category.setUpdatedAt(LocalDateTime.now());
workCategoryMapper.updateById(category);
return Result.success();
}
@Data
public static class CategoryDTO {
private Long parentId;
private String name;
private String icon;
private Integer sort;
private Integer status;
}
}

View File

@@ -0,0 +1,425 @@
package com.dora.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.annotation.RequirePermission;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import com.dora.config.CosConfig;
import com.dora.entity.*;
import com.dora.mapper.*;
import com.dora.vo.PageVO;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 系统配置控制器
*/
@Slf4j
@Tag(name = "系统配置", description = "奖励语句、VIP套餐、积分套餐、Banner、公告管理")
@RestController
@RequestMapping("/admin/config")
@RequiredArgsConstructor
public class AdminConfigController {
private final RewardMessageMapper rewardMessageMapper;
private final PromotionConfigMapper promotionConfigMapper;
private final VipPackageMapper vipPackageMapper;
private final PointsPackageMapper pointsPackageMapper;
private final BannerMapper bannerMapper;
private final NoticeMapper noticeMapper;
private final COSClient cosClient;
private final CosConfig cosConfig;
// ==================== 奖励配置(综合) ====================
@Operation(summary = "获取奖励配置")
@GetMapping("/reward")
@RequirePermission("config:reward")
public Result<Map<String, Object>> getRewardConfig() {
Map<String, Object> result = new HashMap<>();
// 从推广配置中获取奖励积分
List<PromotionConfig> promotions = promotionConfigMapper.selectList(
new LambdaQueryWrapper<PromotionConfig>().orderByAsc(PromotionConfig::getType)
);
for (PromotionConfig config : promotions) {
// type: 1=注册奖励, 2=首充奖励, 3=邀请奖励
if (config.getType() == 1) {
result.put("registerReward", config.getRewardPoints() != null ? config.getRewardPoints() : 0);
} else if (config.getType() == 2) {
result.put("firstChargeReward", config.getRewardPoints() != null ? config.getRewardPoints() : 0);
} else if (config.getType() == 3) {
result.put("inviteReward", config.getRewardPoints() != null ? config.getRewardPoints() : 0);
}
}
// 设置默认值
result.putIfAbsent("registerReward", 100);
result.putIfAbsent("firstChargeReward", 200);
result.putIfAbsent("inviteReward", 50);
return Result.success(result);
}
@Operation(summary = "更新奖励配置")
@PutMapping("/reward")
@RequirePermission("config:reward")
public Result<Void> updateRewardConfig(@RequestBody Map<String, Integer> config) {
// 更新注册奖励
if (config.containsKey("registerReward")) {
updatePromotionReward(1, config.get("registerReward"), "新用户注册奖励");
}
// 更新首充奖励
if (config.containsKey("firstChargeReward")) {
updatePromotionReward(2, config.get("firstChargeReward"), "首次充值奖励");
}
// 更新邀请奖励
if (config.containsKey("inviteReward")) {
updatePromotionReward(3, config.get("inviteReward"), "邀请好友奖励");
}
return Result.success();
}
private void updatePromotionReward(Integer type, Integer points, String description) {
PromotionConfig existing = promotionConfigMapper.selectOne(
new LambdaQueryWrapper<PromotionConfig>().eq(PromotionConfig::getType, type)
);
if (existing != null) {
existing.setRewardPoints(points);
existing.setUpdatedAt(LocalDateTime.now());
promotionConfigMapper.updateById(existing);
} else {
PromotionConfig config = new PromotionConfig();
config.setType(type);
config.setRewardPoints(points);
config.setDescription(description);
config.setStatus(1);
config.setCreatedAt(LocalDateTime.now());
config.setUpdatedAt(LocalDateTime.now());
promotionConfigMapper.insert(config);
}
}
// ==================== 奖励语句配置 ====================
@Operation(summary = "获取奖励语句配置")
@GetMapping("/reward-messages")
@RequirePermission("config:reward")
public Result<Map<String, String>> getRewardMessages() {
List<RewardMessage> list = rewardMessageMapper.selectList(null);
Map<String, String> result = new HashMap<>();
for (RewardMessage msg : list) {
result.put(msg.getConfigKey(), msg.getConfigValue());
}
return Result.success(result);
}
@Operation(summary = "更新奖励语句配置")
@PutMapping("/reward-messages")
@RequirePermission("config:reward")
public Result<Void> updateRewardMessages(@RequestBody Map<String, String> messages) {
for (Map.Entry<String, String> entry : messages.entrySet()) {
RewardMessage existing = rewardMessageMapper.selectByKey(entry.getKey());
if (existing != null) {
existing.setConfigValue(entry.getValue());
existing.setUpdatedAt(LocalDateTime.now());
rewardMessageMapper.updateById(existing);
} else {
RewardMessage msg = new RewardMessage();
msg.setConfigKey(entry.getKey());
msg.setConfigValue(entry.getValue());
msg.setCreatedAt(LocalDateTime.now());
msg.setUpdatedAt(LocalDateTime.now());
rewardMessageMapper.insert(msg);
}
}
return Result.success();
}
// ==================== 推广配置 ====================
@Operation(summary = "获取推广配置")
@GetMapping("/promotion")
@RequirePermission("config:reward")
public Result<List<PromotionConfig>> getPromotionConfig() {
List<PromotionConfig> list = promotionConfigMapper.selectList(
new LambdaQueryWrapper<PromotionConfig>().orderByAsc(PromotionConfig::getType)
);
return Result.success(list);
}
@Operation(summary = "更新推广配置")
@PutMapping("/promotion")
@RequirePermission("config:reward")
public Result<Void> updatePromotionConfig(@RequestBody List<PromotionConfig> configs) {
for (PromotionConfig config : configs) {
config.setUpdatedAt(LocalDateTime.now());
if (config.getId() != null) {
promotionConfigMapper.updateById(config);
} else {
config.setCreatedAt(LocalDateTime.now());
promotionConfigMapper.insert(config);
}
}
return Result.success();
}
// ==================== VIP套餐 ====================
@Operation(summary = "VIP套餐列表")
@GetMapping("/vip-package/list")
@RequirePermission("config:vip")
public Result<List<VipPackage>> getVipPackageList() {
List<VipPackage> list = vipPackageMapper.selectList(
new LambdaQueryWrapper<VipPackage>().orderByAsc(VipPackage::getSort)
);
return Result.success(list);
}
@Operation(summary = "创建VIP套餐")
@PostMapping("/vip-package")
@RequirePermission("config:vip")
public Result<Void> createVipPackage(@RequestBody VipPackage pkg) {
pkg.setCreatedAt(LocalDateTime.now());
pkg.setUpdatedAt(LocalDateTime.now());
vipPackageMapper.insert(pkg);
return Result.success();
}
@Operation(summary = "更新VIP套餐")
@PutMapping("/vip-package/{id}")
@RequirePermission("config:vip")
public Result<Void> updateVipPackage(@PathVariable Long id, @RequestBody VipPackage pkg) {
VipPackage existing = vipPackageMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
pkg.setId(id);
pkg.setUpdatedAt(LocalDateTime.now());
vipPackageMapper.updateById(pkg);
return Result.success();
}
@Operation(summary = "删除VIP套餐")
@DeleteMapping("/vip-package/{id}")
@RequirePermission("config:vip")
public Result<Void> deleteVipPackage(@PathVariable Long id) {
vipPackageMapper.deleteById(id);
return Result.success();
}
// ==================== 积分套餐 ====================
@Operation(summary = "积分套餐列表")
@GetMapping("/points-package/list")
@RequirePermission("config:points")
public Result<List<PointsPackage>> getPointsPackageList() {
List<PointsPackage> list = pointsPackageMapper.selectList(
new LambdaQueryWrapper<PointsPackage>().orderByAsc(PointsPackage::getSort)
);
return Result.success(list);
}
@Operation(summary = "创建积分套餐")
@PostMapping("/points-package")
@RequirePermission("config:points")
public Result<Void> createPointsPackage(@RequestBody PointsPackage pkg) {
pkg.setCreatedAt(LocalDateTime.now());
pkg.setUpdatedAt(LocalDateTime.now());
pointsPackageMapper.insert(pkg);
return Result.success();
}
@Operation(summary = "更新积分套餐")
@PutMapping("/points-package/{id}")
@RequirePermission("config:points")
public Result<Void> updatePointsPackage(@PathVariable Long id, @RequestBody PointsPackage pkg) {
PointsPackage existing = pointsPackageMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
pkg.setId(id);
pkg.setUpdatedAt(LocalDateTime.now());
pointsPackageMapper.updateById(pkg);
return Result.success();
}
@Operation(summary = "删除积分套餐")
@DeleteMapping("/points-package/{id}")
@RequirePermission("config:points")
public Result<Void> deletePointsPackage(@PathVariable Long id) {
pointsPackageMapper.deleteById(id);
return Result.success();
}
// ==================== Banner管理 ====================
@Operation(summary = "Banner列表")
@GetMapping("/banner/list")
@RequirePermission("config:banner")
public Result<List<Banner>> getBannerList() {
List<Banner> list = bannerMapper.selectList(
new LambdaQueryWrapper<Banner>().orderByAsc(Banner::getSort)
);
return Result.success(list);
}
@Operation(summary = "创建Banner")
@PostMapping("/banner")
@RequirePermission("config:banner")
public Result<Void> createBanner(@RequestBody Banner banner) {
banner.setId(null);
banner.setCreatedAt(LocalDateTime.now());
banner.setUpdatedAt(LocalDateTime.now());
bannerMapper.insert(banner);
return Result.success();
}
@Operation(summary = "更新Banner")
@PutMapping("/banner/{id}")
@RequirePermission("config:banner")
public Result<Void> updateBanner(@PathVariable Long id, @RequestBody Banner banner) {
Banner existing = bannerMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
banner.setId(id);
banner.setUpdatedAt(LocalDateTime.now());
bannerMapper.updateById(banner);
return Result.success();
}
@Operation(summary = "删除Banner")
@DeleteMapping("/banner/{id}")
@RequirePermission("config:banner")
public Result<Void> deleteBanner(@PathVariable Long id) {
bannerMapper.deleteById(id);
return Result.success();
}
@Operation(summary = "上传Banner图片")
@PostMapping("/banner/upload")
@RequirePermission("config:banner")
public Result<String> uploadBannerImage(@RequestParam("file") MultipartFile file) {
try {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new BusinessException(ResultCode.PARAM_ERROR, "只能上传图片文件");
}
// 验证文件大小限制10MB
if (file.getSize() > 10 * 1024 * 1024) {
throw new BusinessException(ResultCode.PARAM_ERROR, "图片大小不能超过10MB");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String suffix = "";
if (originalFilename != null && originalFilename.contains(".")) {
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
} else {
// 默认使用jpg
suffix = ".jpg";
}
String fileName = "banner/" + UUID.randomUUID().toString().replace("-", "") + suffix;
// 设置文件元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 上传到COS
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
fileName,
file.getInputStream(),
metadata
);
cosClient.putObject(putObjectRequest);
// 返回文件访问URL
String fileUrl = cosConfig.getFileUrl(fileName);
log.info("Banner图片上传成功: {}", fileUrl);
return Result.success(fileUrl);
} catch (IOException e) {
log.error("Banner图片上传失败", e);
throw new BusinessException(ResultCode.SYSTEM_ERROR, "图片上传失败");
}
}
// ==================== 公告管理 ====================
@Operation(summary = "公告列表")
@GetMapping("/notice/list")
@RequirePermission("config:notice")
public Result<PageVO<Notice>> getNoticeList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
Page<Notice> pageParam = new Page<>(page, pageSize);
Page<Notice> result = noticeMapper.selectPage(pageParam,
new LambdaQueryWrapper<Notice>().orderByDesc(Notice::getCreatedAt)
);
return Result.success(PageVO.of(result.getRecords(), result.getTotal(), page, pageSize));
}
@Operation(summary = "创建公告")
@PostMapping("/notice")
@RequirePermission("config:notice")
public Result<Void> createNotice(@RequestBody Notice notice) {
notice.setCreatedAt(LocalDateTime.now());
notice.setUpdatedAt(LocalDateTime.now());
noticeMapper.insert(notice);
return Result.success();
}
@Operation(summary = "更新公告")
@PutMapping("/notice/{id}")
@RequirePermission("config:notice")
public Result<Void> updateNotice(@PathVariable Long id, @RequestBody Notice notice) {
Notice existing = noticeMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
notice.setId(id);
notice.setUpdatedAt(LocalDateTime.now());
noticeMapper.updateById(notice);
return Result.success();
}
@Operation(summary = "删除公告")
@DeleteMapping("/notice/{id}")
@RequirePermission("config:notice")
public Result<Void> deleteNotice(@PathVariable Long id) {
noticeMapper.deleteById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,43 @@
package com.dora.controller.admin;
import com.dora.common.result.Result;
import com.dora.vo.admin.DashboardStatsVO;
import com.dora.vo.admin.DashboardTrendVO;
import com.dora.vo.admin.RecentOrderVO;
import com.dora.service.AdminDashboardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 管理后台Dashboard控制器
*/
@Tag(name = "Dashboard", description = "控制台数据统计")
@RestController
@RequestMapping("/admin/dashboard")
@RequiredArgsConstructor
public class AdminDashboardController {
private final AdminDashboardService dashboardService;
@Operation(summary = "获取统计数据")
@GetMapping("/stats")
public Result<DashboardStatsVO> getStats() {
return Result.success(dashboardService.getStats());
}
@Operation(summary = "获取近7天趋势数据")
@GetMapping("/trend")
public Result<List<DashboardTrendVO>> getTrend() {
return Result.success(dashboardService.getTrend());
}
@Operation(summary = "获取最近订单")
@GetMapping("/recent-orders")
public Result<List<RecentOrderVO>> getRecentOrders() {
return Result.success(dashboardService.getRecentOrders());
}
}

View File

@@ -0,0 +1,44 @@
package com.dora.controller.admin;
import com.dora.common.result.Result;
import com.dora.dto.admin.OrderQueryDTO;
import com.dora.service.AdminOrderService;
import com.dora.vo.admin.OrderListVO;
import com.dora.vo.admin.OrderVO;
import com.dora.vo.admin.RecentOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 管理后台订单控制器
*/
@Tag(name = "订单管理", description = "订单查询与管理")
@RestController
@RequestMapping("/admin/order")
@RequiredArgsConstructor
public class AdminOrderController {
private final AdminOrderService orderService;
@Operation(summary = "订单列表")
@GetMapping("/list")
public Result<OrderListVO> getOrderList(OrderQueryDTO dto) {
return Result.success(orderService.getOrderList(dto));
}
@Operation(summary = "订单详情")
@GetMapping("/{id}")
public Result<OrderVO> getOrderDetail(@PathVariable Long id, @RequestParam Integer type) {
return Result.success(orderService.getOrderDetail(id, type));
}
@Operation(summary = "最近订单")
@GetMapping("/recent")
public Result<List<RecentOrderVO>> getRecentOrders() {
return Result.success(orderService.getRecentOrders());
}
}

View File

@@ -0,0 +1,69 @@
package com.dora.controller.admin;
import com.dora.annotation.RequirePermission;
import com.dora.common.result.Result;
import com.dora.dto.admin.PointsPackageDTO;
import com.dora.entity.PointsPackage;
import com.dora.service.AdminPointsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "管理端-积分套餐管理")
@RestController
@RequestMapping("/admin/points")
@RequiredArgsConstructor
public class AdminPointsController {
private final AdminPointsService adminPointsService;
@Operation(summary = "获取套餐列表")
@GetMapping("/packages")
@RequirePermission("config:points:list")
public Result<List<PointsPackage>> getPackages() {
return Result.success(adminPointsService.getPackageList());
}
@Operation(summary = "获取套餐详情")
@GetMapping("/packages/{id}")
@RequirePermission("config:points:list")
public Result<PointsPackage> getPackage(@PathVariable Long id) {
return Result.success(adminPointsService.getPackageById(id));
}
@Operation(summary = "创建套餐")
@PostMapping("/packages")
@RequirePermission("config:points:create")
public Result<Void> createPackage(@Valid @RequestBody PointsPackageDTO dto) {
adminPointsService.createPackage(dto);
return Result.success();
}
@Operation(summary = "更新套餐")
@PutMapping("/packages")
@RequirePermission("config:points:update")
public Result<Void> updatePackage(@Valid @RequestBody PointsPackageDTO dto) {
adminPointsService.updatePackage(dto);
return Result.success();
}
@Operation(summary = "删除套餐")
@DeleteMapping("/packages/{id}")
@RequirePermission("config:points:delete")
public Result<Void> deletePackage(@PathVariable Long id) {
adminPointsService.deletePackage(id);
return Result.success();
}
@Operation(summary = "更新套餐状态")
@PutMapping("/packages/{id}/status")
@RequirePermission("config:points:update")
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
adminPointsService.updateStatus(id, status);
return Result.success();
}
}

View File

@@ -0,0 +1,79 @@
package com.dora.controller.admin;
import com.dora.annotation.RequirePermission;
import com.dora.common.result.Result;
import com.dora.dto.admin.RedeemCodeCreateDTO;
import com.dora.dto.admin.RedeemCodeQueryDTO;
import com.dora.entity.RedeemCode;
import com.dora.service.RedeemCodeService;
import com.dora.vo.PageVO;
import com.dora.vo.admin.RedeemCodeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 管理后台兑换码控制器
*/
@Tag(name = "兑换码管理", description = "兑换码的生成、查询、管理")
@RestController
@RequestMapping("/admin/redeem-code")
@RequiredArgsConstructor
public class AdminRedeemCodeController {
private final RedeemCodeService redeemCodeService;
@Operation(summary = "兑换码列表")
@GetMapping("/list")
@RequirePermission("config:redeem")
public Result<PageVO<RedeemCodeVO>> getCodeList(RedeemCodeQueryDTO dto) {
return Result.success(redeemCodeService.getCodeList(dto));
}
@Operation(summary = "兑换码详情")
@GetMapping("/{id}")
@RequirePermission("config:redeem")
public Result<RedeemCodeVO> getCodeDetail(@PathVariable Long id) {
return Result.success(redeemCodeService.getCodeDetail(id));
}
@Operation(summary = "批量生成兑换码")
@PostMapping("/generate")
@RequirePermission("config:redeem")
public Result<List<String>> generateCodes(@RequestBody RedeemCodeCreateDTO dto) {
return Result.success(redeemCodeService.generateCodes(dto));
}
@Operation(summary = "更新兑换码")
@PutMapping("/{id}")
@RequirePermission("config:redeem")
public Result<Void> updateCode(@PathVariable Long id, @RequestBody RedeemCode code) {
redeemCodeService.updateCode(id, code);
return Result.success();
}
@Operation(summary = "删除兑换码")
@DeleteMapping("/{id}")
@RequirePermission("config:redeem")
public Result<Void> deleteCode(@PathVariable Long id) {
redeemCodeService.deleteCode(id);
return Result.success();
}
@Operation(summary = "启用/禁用兑换码")
@PutMapping("/{id}/status")
@RequirePermission("config:redeem")
public Result<Void> toggleStatus(@PathVariable Long id, @RequestBody StatusDTO dto) {
redeemCodeService.toggleStatus(id, dto.getStatus());
return Result.success();
}
@Data
public static class StatusDTO {
private Integer status;
}
}

View File

@@ -0,0 +1,250 @@
package com.dora.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.annotation.RequirePermission;
import com.dora.common.context.AdminContext;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import com.dora.dto.admin.*;
import com.dora.entity.*;
import com.dora.mapper.*;
import com.dora.service.AdminService;
import com.dora.vo.PageVO;
import com.dora.vo.admin.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 系统管理控制器
*/
@Tag(name = "系统管理", description = "管理员、角色、权限管理")
@RestController
@RequestMapping("/admin/system")
@RequiredArgsConstructor
public class AdminSystemController {
private final AdminService adminService;
private final AdminMapper adminMapper;
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
private final AdminRoleMapper adminRoleMapper;
private final RolePermissionMapper rolePermissionMapper;
// ==================== 管理员管理 ====================
@Operation(summary = "管理员列表")
@GetMapping("/admin/list")
@RequirePermission("system:admin:view")
public Result<PageVO<AdminVO>> getAdminList(AdminQueryDTO dto) {
return Result.success(adminService.getAdminList(dto));
}
@Operation(summary = "创建管理员")
@PostMapping("/admin")
@RequirePermission("system:admin:add")
public Result<Void> createAdmin(@Valid @RequestBody AdminCreateDTO dto) {
adminService.createAdmin(dto);
return Result.success();
}
@Operation(summary = "更新管理员")
@PutMapping("/admin/{id}")
@RequirePermission("system:admin:edit")
public Result<Void> updateAdmin(@PathVariable Long id, @Valid @RequestBody AdminUpdateDTO dto) {
adminService.updateAdmin(id, dto);
return Result.success();
}
@Operation(summary = "删除管理员")
@DeleteMapping("/admin/{id}")
@RequirePermission("system:admin:delete")
public Result<Void> deleteAdmin(@PathVariable Long id) {
// 不能删除自己
if (id.equals(AdminContext.getAdminId())) {
throw new BusinessException(ResultCode.OPERATION_NOT_ALLOWED, "不能删除自己");
}
adminService.deleteAdmin(id);
return Result.success();
}
// ==================== 角色管理 ====================
@Operation(summary = "角色列表(分页)")
@GetMapping("/role/list")
@RequirePermission("system:role:view")
public Result<PageVO<RoleVO>> getRoleList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {
Page<Role> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.like(Role::getName, keyword).or().like(Role::getCode, keyword);
}
wrapper.orderByAsc(Role::getSort);
Page<Role> result = roleMapper.selectPage(pageParam, wrapper);
List<RoleVO> list = result.getRecords().stream().map(r -> {
RoleVO vo = new RoleVO();
BeanUtils.copyProperties(r, vo);
return vo;
}).collect(Collectors.toList());
return Result.success(PageVO.of(list, result.getTotal(), page, pageSize));
}
@Operation(summary = "所有角色(下拉选择用)")
@GetMapping("/role/all")
public Result<List<RoleVO>> getAllRoles() {
List<Role> roles = roleMapper.selectList(
new LambdaQueryWrapper<Role>()
.eq(Role::getStatus, 1)
.orderByAsc(Role::getSort)
);
List<RoleVO> list = roles.stream().map(r -> {
RoleVO vo = new RoleVO();
BeanUtils.copyProperties(r, vo);
return vo;
}).collect(Collectors.toList());
return Result.success(list);
}
@Operation(summary = "创建角色")
@PostMapping("/role")
@RequirePermission("system:role:add")
public Result<Void> createRole(@RequestBody Role role) {
// 检查编码唯一性
Role existing = roleMapper.selectByCode(role.getCode());
if (existing != null) {
throw new BusinessException(ResultCode.DATA_ALREADY_EXISTS, "角色编码已存在");
}
role.setCreatedAt(LocalDateTime.now());
role.setUpdatedAt(LocalDateTime.now());
roleMapper.insert(role);
return Result.success();
}
@Operation(summary = "更新角色")
@PutMapping("/role/{id}")
@RequirePermission("system:role:edit")
public Result<Void> updateRole(@PathVariable Long id, @RequestBody Role role) {
Role existing = roleMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
role.setId(id);
role.setCode(existing.getCode()); // 编码不允许修改
role.setUpdatedAt(LocalDateTime.now());
roleMapper.updateById(role);
return Result.success();
}
@Operation(summary = "删除角色")
@DeleteMapping("/role/{id}")
@RequirePermission("system:role:delete")
public Result<Void> deleteRole(@PathVariable Long id) {
// 检查是否有管理员使用该角色
Long count = adminRoleMapper.selectCount(
new LambdaQueryWrapper<AdminRole>().eq(AdminRole::getRoleId, id)
);
if (count > 0) {
throw new BusinessException(ResultCode.OPERATION_NOT_ALLOWED, "该角色下有管理员,无法删除");
}
roleMapper.deleteById(id);
rolePermissionMapper.deleteByRoleId(id);
return Result.success();
}
@Operation(summary = "获取角色权限")
@GetMapping("/role/{id}/permissions")
@RequirePermission("system:role:view")
public Result<List<Long>> getRolePermissions(@PathVariable Long id) {
List<Long> permissionIds = permissionMapper.selectPermissionIdsByRoleId(id);
return Result.success(permissionIds);
}
@Operation(summary = "更新角色权限")
@PutMapping("/role/{id}/permissions")
@RequirePermission("system:role:permission")
public Result<Void> updateRolePermissions(@PathVariable Long id, @RequestBody List<Long> permissionIds) {
// 删除原有权限
rolePermissionMapper.deleteByRoleId(id);
// 添加新权限
for (Long permissionId : permissionIds) {
RolePermission rp = new RolePermission();
rp.setRoleId(id);
rp.setPermissionId(permissionId);
rp.setCreatedAt(LocalDateTime.now());
rolePermissionMapper.insert(rp);
}
return Result.success();
}
// ==================== 权限管理 ====================
@Operation(summary = "权限树")
@GetMapping("/permission/tree")
@RequirePermission("system:permission:view")
public Result<List<PermissionTreeVO>> getPermissionTree() {
return Result.success(adminService.getPermissionTree());
}
@Operation(summary = "创建权限")
@PostMapping("/permission")
@RequirePermission("system:permission:add")
public Result<Void> createPermission(@RequestBody Permission permission) {
// 检查编码唯一性
Permission existing = permissionMapper.selectOne(
new LambdaQueryWrapper<Permission>().eq(Permission::getCode, permission.getCode())
);
if (existing != null) {
throw new BusinessException(ResultCode.DATA_ALREADY_EXISTS, "权限编码已存在");
}
permission.setCreatedAt(LocalDateTime.now());
permission.setUpdatedAt(LocalDateTime.now());
permissionMapper.insert(permission);
return Result.success();
}
@Operation(summary = "更新权限")
@PutMapping("/permission/{id}")
@RequirePermission("system:permission:edit")
public Result<Void> updatePermission(@PathVariable Long id, @RequestBody Permission permission) {
Permission existing = permissionMapper.selectById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND);
}
permission.setId(id);
permission.setCode(existing.getCode()); // 编码不允许修改
permission.setUpdatedAt(LocalDateTime.now());
permissionMapper.updateById(permission);
return Result.success();
}
@Operation(summary = "删除权限")
@DeleteMapping("/permission/{id}")
@RequirePermission("system:permission:delete")
public Result<Void> deletePermission(@PathVariable Long id) {
// 检查是否有子权限
Long childCount = permissionMapper.selectCount(
new LambdaQueryWrapper<Permission>().eq(Permission::getParentId, id)
);
if (childCount > 0) {
throw new BusinessException(ResultCode.OPERATION_NOT_ALLOWED, "存在子权限,无法删除");
}
permissionMapper.deleteById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,111 @@
package com.dora.controller.admin;
import com.dora.common.result.Result;
import com.dora.config.CosConfig;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Slf4j
@Tag(name = "管理端-文件上传")
@RestController
@RequestMapping("/admin/upload")
@RequiredArgsConstructor
public class AdminUploadController {
private final COSClient cosClient;
private final CosConfig cosConfig;
@Operation(summary = "上传单个图片")
@PostMapping("/image")
public Result<String> uploadImage(@RequestParam("file") MultipartFile file) {
try {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return Result.fail("只能上传图片文件");
}
// 验证文件大小10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.fail("图片大小不能超过10MB");
}
String url = uploadToCos(file);
return Result.success(url);
} catch (Exception e) {
log.error("上传图片失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
@Operation(summary = "上传多个图片")
@PostMapping("/images")
public Result<List<String>> uploadImages(@RequestParam("files") MultipartFile[] files) {
try {
List<String> urls = new ArrayList<>();
for (MultipartFile file : files) {
// 验证文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return Result.fail("只能上传图片文件");
}
// 验证文件大小10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.fail("图片大小不能超过10MB");
}
String url = uploadToCos(file);
urls.add(url);
}
return Result.success(urls);
} catch (Exception e) {
log.error("上传图片失败", e);
return Result.fail("上传失败: " + e.getMessage());
}
}
private String uploadToCos(MultipartFile file) throws IOException {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String fileName = "ai-debug/" + UUID.randomUUID().toString() + extension;
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 上传到COS
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
fileName,
file.getInputStream(),
metadata
);
cosClient.putObject(putObjectRequest);
// 返回文件URL
return cosConfig.getFileUrl(fileName);
}
}

View File

@@ -0,0 +1,155 @@
package com.dora.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dora.annotation.RequirePermission;
import com.dora.common.context.AdminContext;
import com.dora.common.exception.BusinessException;
import com.dora.common.result.Result;
import com.dora.common.result.ResultCode;
import com.dora.entity.PointsRecord;
import com.dora.entity.User;
import com.dora.mapper.PointsRecordMapper;
import com.dora.mapper.UserMapper;
import com.dora.vo.PageVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户管理控制器
*/
@Tag(name = "用户管理", description = "用户列表、状态管理、积分调整")
@RestController
@RequestMapping("/admin/user")
@RequiredArgsConstructor
public class AdminUserController {
private final UserMapper userMapper;
private final PointsRecordMapper pointsRecordMapper;
@Operation(summary = "用户列表")
@GetMapping("/list")
@RequirePermission("user:view")
public Result<PageVO<User>> getUserList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer vipLevel,
@RequestParam(required = false) Integer status) {
Page<User> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w.like(User::getNickname, keyword)
.or().like(User::getPhone, keyword));
}
if (vipLevel != null) {
wrapper.eq(User::getVipLevel, vipLevel);
}
if (status != null) {
wrapper.eq(User::getStatus, status);
}
wrapper.orderByDesc(User::getCreatedAt);
Page<User> result = userMapper.selectPage(pageParam, wrapper);
return Result.success(PageVO.of(result.getRecords(), result.getTotal(), page, pageSize));
}
@Operation(summary = "用户详情")
@GetMapping("/{id}")
@RequirePermission("user:view")
public Result<User> getUserDetail(@PathVariable Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
return Result.success(user);
}
@Operation(summary = "更新用户状态")
@PutMapping("/{id}/status")
@RequirePermission("user:edit")
public Result<Void> updateUserStatus(@PathVariable Long id, @RequestBody StatusDTO dto) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
user.setStatus(dto.getStatus());
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
return Result.success();
}
@Operation(summary = "调整用户积分")
@PostMapping("/{id}/points")
@RequirePermission("user:points")
public Result<Void> adjustUserPoints(@PathVariable Long id, @RequestBody PointsAdjustDTO dto) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
int newPoints = user.getPoints() + dto.getPoints();
if (newPoints < 0) {
throw new BusinessException(ResultCode.OPERATION_NOT_ALLOWED, "积分不足");
}
// 更新用户积分
user.setPoints(newPoints);
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
// 记录积分流水
PointsRecord record = new PointsRecord();
record.setUserId(id);
record.setType(dto.getPoints() > 0 ? 3 : 2); // 3赠送 2消费
record.setPoints(dto.getPoints());
record.setBalance(newPoints);
record.setBizType("admin_adjust");
record.setRemark(dto.getRemark() != null ? dto.getRemark() : "管理员调整");
record.setCreatedAt(LocalDateTime.now());
pointsRecordMapper.insert(record);
return Result.success();
}
@Operation(summary = "更新用户VIP")
@PutMapping("/{id}/vip")
@RequirePermission("user:vip")
public Result<Void> updateUserVip(@PathVariable Long id, @RequestBody VipUpdateDTO dto) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
user.setVipLevel(dto.getVipLevel());
user.setVipExpireTime(dto.getVipExpireTime());
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
return Result.success();
}
@Data
public static class StatusDTO {
private Integer status;
}
@Data
public static class PointsAdjustDTO {
private Integer points;
private String remark;
}
@Data
public static class VipUpdateDTO {
private Integer vipLevel;
private LocalDateTime vipExpireTime;
}
}

View File

@@ -0,0 +1,87 @@
package com.dora.controller.admin;
import com.dora.common.result.Result;
import com.dora.dto.admin.AdminWorkQueryDTO;
import com.dora.service.AdminWorkService;
import com.dora.vo.PageVO;
import com.dora.vo.admin.AdminWorkVO;
import com.dora.vo.admin.AdminWorkStatsVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 管理后台广场作品控制器
*/
@Tag(name = "广场作品管理", description = "广场作品的查询、审核、精选等管理")
@RestController
@RequestMapping("/admin/work")
@RequiredArgsConstructor
public class AdminWorkController {
private final AdminWorkService adminWorkService;
@Operation(summary = "广场作品列表")
@GetMapping("/list")
public Result<PageVO<AdminWorkVO>> getWorkList(AdminWorkQueryDTO dto) {
return Result.success(adminWorkService.getWorkList(dto));
}
@Operation(summary = "广场作品统计")
@GetMapping("/stats")
public Result<AdminWorkStatsVO> getWorkStats() {
return Result.success(adminWorkService.getWorkStats());
}
@Operation(summary = "作品详情")
@GetMapping("/{id}")
public Result<AdminWorkVO> getWorkDetail(@PathVariable Long id) {
return Result.success(adminWorkService.getWorkDetail(id));
}
@Operation(summary = "审核作品")
@PutMapping("/{id}/audit")
public Result<Void> auditWork(@PathVariable Long id, @RequestBody AuditDTO dto) {
adminWorkService.auditWork(id, dto.getAuditStatus(), dto.getAuditRemark());
return Result.success();
}
@Operation(summary = "设置/取消精选")
@PutMapping("/{id}/featured")
public Result<Void> setFeatured(@PathVariable Long id, @RequestBody FeaturedDTO dto) {
adminWorkService.setFeatured(id, dto.getIsFeatured());
return Result.success();
}
@Operation(summary = "下架/上架作品")
@PutMapping("/{id}/status")
public Result<Void> setStatus(@PathVariable Long id, @RequestBody StatusDTO dto) {
adminWorkService.setWorkStatus(id, dto.getStatus());
return Result.success();
}
@Operation(summary = "删除作品")
@DeleteMapping("/{id}")
public Result<Void> deleteWork(@PathVariable Long id) {
adminWorkService.deleteWork(id);
return Result.success();
}
// 内部DTO类
@lombok.Data
public static class AuditDTO {
private Integer auditStatus;
private String auditRemark;
}
@lombok.Data
public static class FeaturedDTO {
private Integer isFeatured;
}
@lombok.Data
public static class StatusDTO {
private Integer status;
}
}

View File

@@ -0,0 +1,61 @@
package com.dora.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Data
public class AiModelDTO {
private Long id;
@NotNull(message = "厂商ID不能为空")
private Long providerId;
@NotBlank(message = "模型名称不能为空")
private String name;
@NotBlank(message = "模型编码不能为空")
private String code;
@NotBlank(message = "模型类型不能为空")
private String type;
private String category;
private String description;
private String icon;
/** 封面图URL */
private String coverImage;
@NotBlank(message = "API端点不能为空")
private String apiEndpoint;
private String requestMethod;
private String requestHeaders;
@NotBlank(message = "请求模板不能为空")
private String requestTemplate;
@NotBlank(message = "响应映射不能为空")
private String responseMapping;
@NotBlank(message = "输入参数配置不能为空")
private String inputParams;
@NotNull(message = "积分消耗不能为空")
private Integer pointsCost;
private Integer maxConcurrent;
private Integer timeout;
private String workflowType;
private String workflowId;
private String workflowConfig;
private Integer isAsync;
/** 是否显示在AI功能列表0不显示 1显示 */
private Integer showInList;
@NotNull(message = "状态不能为空")
private Integer status;
private Integer sort;
}

View File

@@ -0,0 +1,27 @@
package com.dora.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Data
public class AiProviderDTO {
private Long id;
@NotBlank(message = "厂商名称不能为空")
private String name;
@NotBlank(message = "厂商编码不能为空")
private String code;
private String description;
private String baseUrl;
private String apiKey;
private String secretKey;
private String extraConfig;
@NotNull(message = "状态不能为空")
private Integer status;
private Integer sort;
}

View File

@@ -0,0 +1,19 @@
package com.dora.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Data
public class AiTaskDTO {
@NotNull(message = "模型ID不能为空")
private Long modelId;
@NotBlank(message = "输入参数不能为空")
private String inputParams;
private Integer priority;
/** 是否订阅完成通知 */
private Boolean subscribeNotify;
}

View File

@@ -0,0 +1,10 @@
package com.dora.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class CreatePointsOrderDTO {
@NotNull(message = "套餐ID不能为空")
private Long packageId;
}

View File

@@ -0,0 +1,27 @@
package com.dora.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
/**
* 发布作品DTO
*/
@Data
public class PublishWorkDTO {
@NotNull(message = "任务ID不能为空")
private Long taskId;
@NotBlank(message = "作品标题不能为空")
@Size(max = 20, message = "标题长度不能超过20个字符")
private String title;
@Size(max = 5000, message = "描述长度不能超过5000个字符")
private String description;
@NotNull(message = "分类ID不能为空")
private Long categoryId;
}

View File

@@ -0,0 +1,13 @@
package com.dora.dto;
import lombok.Data;
/**
* 刷新Token请求DTO
*/
@Data
public class RefreshTokenDTO {
/** 刷新令牌 */
private String refreshToken;
}

View File

@@ -0,0 +1,18 @@
package com.dora.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 更新用户资料DTO
*/
@Data
@Schema(description = "更新用户资料请求")
public class UpdateProfileDTO {
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像URL")
private String avatar;
}

View File

@@ -0,0 +1,25 @@
package com.dora.dto;
import lombok.Data;
/**
* 作品查询DTO
*/
@Data
public class WorkQueryDTO {
/** 搜索关键词匹配标题、描述、prompt */
private String keyword;
/** 排序类型hot最热 new最新 */
private String sortType = "hot";
/** 分类ID */
private Long categoryId;
/** 页码 */
private Integer pageNum = 1;
/** 每页数量 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,28 @@
package com.dora.dto;
import lombok.Data;
/**
* 微信登录请求DTO
*/
@Data
public class WxLoginDTO {
/** 微信登录code首次登录时使用 */
private String code;
/** 微信openid从/user/check接口获取避免code重复使用 */
private String openid;
/** 用户昵称 */
private String nickname;
/** 用户头像 */
private String avatar;
/** 手机号授权code用于获取手机号 */
private String phoneCode;
/** 邀请码(注册时填写,用于推广奖励) */
private String inviteCode;
}

View File

@@ -0,0 +1,43 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 创建管理员DTO
*/
@Data
@Schema(description = "创建管理员请求")
public class AdminCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
@Schema(description = "用户名", required = true)
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度6-20位")
@Schema(description = "密码", required = true)
private String password;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "手机号")
private String phone;
@Schema(description = "邮箱")
private String email;
@NotEmpty(message = "角色不能为空")
@Schema(description = "角色ID列表", required = true)
private List<Long> roleIds;
@Schema(description = "状态0禁用 1正常", defaultValue = "1")
private Integer status = 1;
}

View File

@@ -0,0 +1,21 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 管理员登录DTO
*/
@Data
@Schema(description = "管理员登录请求")
public class AdminLoginDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", required = true)
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码", required = true)
private String password;
}

View File

@@ -0,0 +1,24 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 管理员查询DTO
*/
@Data
@Schema(description = "管理员查询请求")
public class AdminQueryDTO {
@Schema(description = "关键词(用户名/姓名)")
private String keyword;
@Schema(description = "状态")
private Integer status;
@Schema(description = "页码", defaultValue = "1")
private Integer page = 1;
@Schema(description = "每页数量", defaultValue = "10")
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,33 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理员注册DTO仅用于测试
*/
@Data
@Schema(description = "管理员注册请求")
public class AdminRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
@Schema(description = "用户名", required = true)
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度6-20位")
@Schema(description = "密码", required = true)
private String password;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "手机号")
private String phone;
@Schema(description = "邮箱")
private String email;
}

View File

@@ -0,0 +1,34 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 更新管理员DTO
*/
@Data
@Schema(description = "更新管理员请求")
public class AdminUpdateDTO {
@Schema(description = "密码(留空则不修改)")
private String password;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "手机号")
private String phone;
@Schema(description = "邮箱")
private String email;
@NotEmpty(message = "角色不能为空")
@Schema(description = "角色ID列表", required = true)
private List<Long> roleIds;
@Schema(description = "状态0禁用 1正常")
private Integer status;
}

View File

@@ -0,0 +1,33 @@
package com.dora.dto.admin;
import lombok.Data;
/**
* 管理端作品查询DTO
*/
@Data
public class AdminWorkQueryDTO {
/** 关键词(标题/描述) */
private String keyword;
/** 分类ID */
private Long categoryId;
/** 任务类型 */
private String taskType;
/** 审核状态 */
private Integer auditStatus;
/** 是否精选 */
private Integer isFeatured;
/** 用户ID */
private Long userId;
/** 页码 */
private Integer page = 1;
/** 每页数量 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,19 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 发送邮箱验证码DTO
*/
@Data
@Schema(description = "发送邮箱验证码请求")
public class EmailCodeDTO {
@Schema(description = "邮箱地址", required = true)
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

View File

@@ -0,0 +1,23 @@
package com.dora.dto.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 邮箱验证码登录DTO
*/
@Data
@Schema(description = "邮箱验证码登录请求")
public class EmailLoginDTO {
@Schema(description = "邮箱地址", required = true)
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "验证码", required = true)
@NotBlank(message = "验证码不能为空")
private String code;
}

View File

@@ -0,0 +1,33 @@
package com.dora.dto.admin;
import lombok.Data;
/**
* 订单查询DTO
*/
@Data
public class OrderQueryDTO {
/** 订单号 */
private String orderNo;
/** 用户名 */
private String username;
/** 订单类型1VIP充值 2积分充值 */
private Integer type;
/** 订单状态 */
private Integer status;
/** 开始日期 */
private String startDate;
/** 结束日期 */
private String endDate;
/** 页码 */
private Integer page = 1;
/** 每页数量 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,30 @@
package com.dora.dto.admin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class PointsPackageDTO {
private Long id;
@NotBlank(message = "套餐名称不能为空")
private String name;
@NotNull(message = "积分数量不能为空")
private Integer points;
@NotNull(message = "价格不能为空")
private BigDecimal price;
private BigDecimal originalPrice;
private Integer bonusPoints;
private Integer validDays;
private String description;
private String bgImage;
private String cardStyle;
private String btnStyle;
private Integer sort;
private Integer status;
}

View File

@@ -0,0 +1,38 @@
package com.dora.dto.admin;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 兑换码创建DTO
*/
@Data
public class RedeemCodeCreateDTO {
/** 生成数量 */
private Integer count = 1;
/** 类型1积分 2VIP会员 */
private Integer type;
/** 奖励值积分数量或VIP天数 */
private Integer rewardValue;
/** VIP等级type=2时有效 */
private Integer vipLevel;
/** 每个码可使用次数 */
private Integer totalCount = 1;
/** 生效开始时间 */
private LocalDateTime startTime;
/** 过期时间 */
private LocalDateTime expireTime;
/** 备注 */
private String remark;
/** 自定义兑换码前缀 */
private String prefix;
}

View File

@@ -0,0 +1,30 @@
package com.dora.dto.admin;
import lombok.Data;
/**
* 兑换码查询DTO
*/
@Data
public class RedeemCodeQueryDTO {
/** 兑换码 */
private String code;
/** 批次号 */
private String batchNo;
/** 类型1积分 2VIP会员 */
private Integer type;
/** 状态 */
private Integer status;
/** 是否已用完0否 1是 */
private Integer exhausted;
/** 页码 */
private Integer page = 1;
/** 每页数量 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,4 @@
/**
* 数据传输对象层
*/
package com.dora.dto;

View File

@@ -0,0 +1,15 @@
package com.dora.dto.video;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DialogueItem {
private Long characterId;
private String characterName;
private String characterAvatar;
private String content;
}

View File

@@ -0,0 +1,10 @@
package com.dora.dto.video;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
@Data
public class GenerateScriptDTO {
@NotBlank(message = "创意内容不能为空")
private String idea;
}

View File

@@ -0,0 +1,18 @@
package com.dora.dto.video;
import lombok.Data;
@Data
public class ProjectCharacterDTO {
private Long id;
private Long templateId;
private String name;
private String age;
private String gender;
private String voiceType;
private String appearance;
private String clothing;
private String description;
private String imageUrl;
private String referenceImageUrl;
}

View File

@@ -0,0 +1,13 @@
package com.dora.dto.video;
import lombok.Data;
@Data
public class ProjectSceneDTO {
private Long id;
private String sceneName;
private String sceneTitle;
private String sceneDescription;
private String sceneStory;
private Integer storyboardCount;
}

View File

@@ -0,0 +1,20 @@
package com.dora.dto.video;
import lombok.Data;
import java.util.List;
@Data
public class SceneStoryboardDTO {
private Long id;
private Integer storyboardIndex;
private String shotType;
private String cameraAngle;
private String cameraMove;
private String description;
private String narration;
private String dialogue;
private Long dialogueCharacterId;
private List<DialogueItem> dialogues;
private String imageUrl;
private Integer duration;
}

View File

@@ -0,0 +1,32 @@
package com.dora.dto.video;
import lombok.Data;
import java.util.List;
@Data
public class ScriptGenerationResult {
private String title;
private String summary;
private String content;
private List<CharacterInfo> characters;
private List<SceneInfo> scenes;
@Data
public static class CharacterInfo {
private String name;
private String description;
private String voiceType;
private String age;
private String gender;
private String appearance;
private String clothing;
}
@Data
public static class SceneInfo {
private String name;
private String title;
private String description;
private String story;
}
}

View File

@@ -0,0 +1,22 @@
package com.dora.dto.video;
import lombok.Data;
import java.util.List;
@Data
public class StoryboardGenerationResult {
private List<StoryboardInfo> storyboards;
@Data
public static class StoryboardInfo {
private Integer index;
private String shotType;
private String cameraAngle;
private String cameraMove;
private String description;
private String narration;
private String dialogue;
private String dialogueCharacter;
private Integer duration;
}
}

View File

@@ -0,0 +1,17 @@
package com.dora.dto.video;
import lombok.Data;
@Data
public class VideoProjectDTO {
private Long id;
private String projectName;
private String storyTitle;
private String storyOutline;
private String originalIdea;
private Integer creationMode;
private String videoDuration;
private String videoRatio;
private String videoStyle;
private String coverUrl;
}

View File

@@ -0,0 +1,45 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理员实体
*/
@Data
@TableName("`admin`")
public class Admin implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private String email;
private String avatar;
private Integer status;
private LocalDateTime lastLoginTime;
private String lastLoginIp;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,26 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理员-角色关联实体
*/
@Data
@TableName("admin_role")
public class AdminRole implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long adminId;
private Long roleId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,54 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_model")
public class AiModel {
@TableId(type = IdType.AUTO)
private Long id;
private Long providerId;
private String name;
private String code;
private String type;
private String category;
private String description;
private String icon;
/** 封面图URL */
private String coverImage;
private String apiEndpoint;
private String requestMethod;
private String requestHeaders;
private String requestTemplate;
private String responseMapping;
private String inputParams;
private Integer pointsCost;
private Integer maxConcurrent;
private Integer timeout;
private String workflowType;
private String workflowId;
private String workflowConfig;
private Integer isAsync;
private String asyncQueryEndpoint;
private String asyncQueryMethod;
private String asyncQueryBody;
private String asyncQueryMapping;
private String asyncStatusMapping;
private Integer asyncPollInterval;
private Integer asyncPollMaxCount;
private Integer asyncPollTimeout;
/** 是否将结果URL转存到COS */
private Integer resultTransferCos;
/** 结果中的URL字段路径如 data.remote_url */
private String resultUrlField;
/** 是否显示在AI功能列表0不显示 1显示 */
private Integer showInList;
private Integer status;
private Integer sort;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,25 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("ai_prompt_template")
public class AiPromptTemplate {
@TableId(type = IdType.AUTO)
private Long id;
private String templateCode;
private String templateName;
private String systemPrompt;
private String userPromptTemplate;
private String outputFormat;
private String modelCode;
private BigDecimal temperature;
private Integer maxTokens;
private String description;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,25 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_provider")
public class AiProvider {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String code;
private String description;
private String baseUrl;
private String apiKey;
private String secretKey;
private String extraConfig;
private Integer status;
private Integer sort;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,43 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_task")
public class AiTask {
@TableId(type = IdType.AUTO)
private Long id;
private String taskNo;
private Long userId;
private Long modelId;
private String modelCode;
private String inputParams;
private String outputResult;
private Integer pointsCost;
private Integer status;
private Integer progress;
private String errorMessage;
/** 外部任务ID用于异步任务状态查询 */
private String externalTaskId;
/** 轮询次数 */
private Integer pollCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer duration;
private Integer retryCount;
private Integer maxRetry;
private Integer priority;
private String ip;
/** 是否订阅完成通知0否 1是 */
private Integer subscribeNotify;
/** 通知是否已发送0否 1是 */
private Integer notifySent;
/** 发布状态0未发布 1审核中 2已发布 3审核未通过 */
private Integer publishStatus;
/** 关联的作品ID */
private Long workId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,54 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* AI使用记录实体
*/
@Data
@TableName("ai_usage_record")
public class AiUsageRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
/** 任务类型text2img文生图 img2img图生图 text2video文生视频 img2video图生视频 */
private String taskType;
private String model;
private String prompt;
/** 参考图URL数组JSON格式 */
private String refImages;
private String result;
private Integer tokensUsed;
private Integer pointsCost;
private Integer duration;
/** 状态0队列中 1进行中 2成功 3失败 */
private Integer status;
/** 进度百分比0-100 */
private Integer progress;
private String errorMsg;
private String ip;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,73 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* AI广场作品实体
*/
@Data
@TableName("ai_work")
public class AiWork implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long categoryId;
private String title;
private String description;
private String contentUrl;
private Integer contentType;
/** 任务类型text2img文生图 img2img图生图 text2video文生视频 img2video图生视频 */
private String taskType;
/** AI模型名称 */
private String model;
private String prompt;
private String tags;
private Integer viewCount;
private Integer likeCount;
private Integer collectCount;
private Integer commentCount;
private Integer isPublic;
private Integer auditStatus;
private String auditRemark;
private LocalDateTime auditTime;
private Long auditorId;
private Integer isFeatured;
private Integer featuredSort;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,39 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 作品评论实体
*/
@Data
@TableName("ai_work_comment")
public class AiWorkComment implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long workId;
private Long userId;
private Long parentId;
private Long replyUserId;
private String content;
private Integer likeCount;
private Integer status;
private LocalDateTime createdAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,26 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 作品点赞实体
*/
@Data
@TableName("ai_work_like")
public class AiWorkLike implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long workId;
private Long userId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,40 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 审核记录实体(用于记录审核历史)
*/
@Data
@TableName("audit_record")
public class AuditRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 业务类型work作品 comment评论 */
private String bizType;
/** 业务ID */
private Long bizId;
/** 审核状态0待审核 1通过 2拒绝 */
private Integer status;
/** 审核备注 */
private String remark;
/** 审核人ID */
private Long auditorId;
/** 审核人名称 */
private String auditorName;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,49 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Banner轮播图实体
*/
@Data
@TableName("banner")
public class Banner implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String imageUrl;
private Integer linkType;
private String linkUrl;
private String position;
private Integer sort;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer clickCount;
private Long creatorId;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,29 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("character_template")
public class CharacterTemplate {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String gender;
private String ageRange;
private String voiceType;
private String appearance;
private String clothing;
private String description;
private String imageUrl;
private String category;
private Integer isSystem;
private Long userId;
private Integer sort;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,51 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统公告实体
*/
@Data
@TableName("notice")
public class Notice implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String content;
private Integer type;
private Integer level;
private Integer target;
private Integer isTop;
private Integer isPopup;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer viewCount;
private Long creatorId;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,26 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 公告已读记录实体
*/
@Data
@TableName("notice_read")
public class NoticeRead implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long noticeId;
private Long userId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,47 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 权限实体
*/
@Data
@TableName("permission")
public class Permission implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String name;
private String code;
private Integer type;
private String path;
private String component;
private String icon;
private Integer sort;
private Integer visible;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,25 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("points_order")
public class PointsOrder {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private Long packageId;
private Integer points;
private Integer bonusPoints;
private BigDecimal amount;
private Integer payType;
private LocalDateTime payTime;
private String transactionId;
private Integer status; // 0待支付 1已支付 2已取消 3已退款
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,29 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("points_package")
public class PointsPackage {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer points;
private BigDecimal price;
private BigDecimal originalPrice;
private Integer bonusPoints;
private Integer validDays;
private String description;
private String bgImage;
private String cardStyle;
private String btnStyle;
private Integer sort;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,20 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("points_record")
public class PointsRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Integer type; // 1充值 2消费 3赠送 4推广奖励 5签到 6退款
private Integer points;
private Integer balance;
private String bizType;
private Long bizId;
private String remark;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,34 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("project_character")
public class ProjectCharacter {
@TableId(type = IdType.AUTO)
private Long id;
private Long projectId;
private Long templateId;
private String name;
private String age;
private String gender;
private String voiceType;
private String appearance;
private String clothing;
private String description;
private String imageUrl;
private String referenceImageUrl;
private Integer sort;
/**
* 形象生成状态: 0-无图片, 1-生成中, 2-已生成
*/
private Integer imageStatus;
/**
* 正在执行的任务编号(用于前端轮询)
*/
private String currentTaskNo;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,24 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("project_scene")
public class ProjectScene {
@TableId(type = IdType.AUTO)
private Long id;
private Long projectId;
private String sceneName;
private String sceneTitle;
private String sceneDescription;
private String sceneStory;
private Integer storyboardCount;
private Integer sort;
private String videoUrl; // 场次视频URL
private String videoTaskNo; // 视频生成任务号
private Integer videoStatus; // 视频状态0-未生成1-生成中2-已生成
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,35 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 推广配置实体
*/
@Data
@TableName("promotion_config")
public class PromotionConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Integer type;
private Integer rewardPoints;
private BigDecimal rewardPercent;
private String description;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,34 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 推广记录实体
*/
@Data
@TableName("promotion_record")
public class PromotionRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long inviterId;
private Long inviteeId;
private Integer rewardType;
private Integer rewardPoints;
private Integer rewardStatus;
private LocalDateTime rewardTime;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,63 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 兑换码实体
*/
@Data
@TableName("redeem_code")
public class RedeemCode implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 兑换码 */
private String code;
/** 批次号 */
private String batchNo;
/** 类型1积分 2VIP会员 */
private Integer type;
/** 奖励值积分数量或VIP天数 */
private Integer rewardValue;
/** VIP等级type=2时有效 */
private Integer vipLevel;
/** 可使用次数 */
private Integer totalCount;
/** 已使用次数 */
private Integer usedCount;
/** 生效开始时间 */
private LocalDateTime startTime;
/** 过期时间 */
private LocalDateTime expireTime;
/** 备注 */
private String remark;
/** 创建人ID */
private Long creatorId;
/** 状态0禁用 1启用 */
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,40 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 兑换码使用记录实体
*/
@Data
@TableName("redeem_code_record")
public class RedeemCodeRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 兑换码ID */
private Long codeId;
/** 兑换码 */
private String code;
/** 用户ID */
private Long userId;
/** 类型1积分 2VIP会员 */
private Integer type;
/** 奖励值 */
private Integer rewardValue;
/** IP地址 */
private String ip;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,39 @@
package com.dora.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 奖励语句配置实体
*/
@Data
@TableName("reward_message")
public class RewardMessage implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/**
* 配置键
*/
private String configKey;
/**
* 配置值(奖励语句)
*/
private String configValue;
/**
* 描述
*/
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

Some files were not shown because too many files have changed in this diff Show More