first commit
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal 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
34
README.md
Normal 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
427
docs/CODE_STANDARDS.md
Normal 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
166
pom.xml
Normal 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>
|
||||
19
src/main/java/com/dora/WeixinApplication.java
Normal file
19
src/main/java/com/dora/WeixinApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/dora/annotation/RequirePermission.java
Normal file
22
src/main/java/com/dora/annotation/RequirePermission.java
Normal 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逻辑),默认false(OR逻辑)
|
||||
*/
|
||||
boolean requireAll() default false;
|
||||
}
|
||||
78
src/main/java/com/dora/aspect/PermissionAspect.java
Normal file
78
src/main/java/com/dora/aspect/PermissionAspect.java
Normal 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();
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/dora/common/context/AdminContext.java
Normal file
31
src/main/java/com/dora/common/context/AdminContext.java
Normal 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();
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/dora/common/context/UserContext.java
Normal file
46
src/main/java/com/dora/common/context/UserContext.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/dora/common/result/Result.java
Normal file
59
src/main/java/com/dora/common/result/Result.java
Normal 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;
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/dora/common/result/ResultCode.java
Normal file
81
src/main/java/com/dora/common/result/ResultCode.java
Normal 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;
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/dora/config/CosConfig.java
Normal file
47
src/main/java/com/dora/config/CosConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/dora/config/DatabaseInitConfig.java
Normal file
63
src/main/java/com/dora/config/DatabaseInitConfig.java
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
28
src/main/java/com/dora/config/MyBatisPlusConfig.java
Normal file
28
src/main/java/com/dora/config/MyBatisPlusConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/dora/config/RedisConfig.java
Normal file
46
src/main/java/com/dora/config/RedisConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/dora/config/RestTemplateConfig.java
Normal file
21
src/main/java/com/dora/config/RestTemplateConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/dora/config/SwaggerConfig.java
Normal file
63
src/main/java/com/dora/config/SwaggerConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
155
src/main/java/com/dora/config/WebConfig.java
Normal file
155
src/main/java/com/dora/config/WebConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/dora/config/WechatConfig.java
Normal file
46
src/main/java/com/dora/config/WechatConfig.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/dora/config/WxPayConfig.java
Normal file
36
src/main/java/com/dora/config/WxPayConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
172
src/main/java/com/dora/controller/AiController.java
Normal file
172
src/main/java/com/dora/controller/AiController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/dora/controller/BannerController.java
Normal file
37
src/main/java/com/dora/controller/BannerController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/dora/controller/CategoryController.java
Normal file
32
src/main/java/com/dora/controller/CategoryController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/dora/controller/HealthController.java
Normal file
27
src/main/java/com/dora/controller/HealthController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
225
src/main/java/com/dora/controller/NoticeController.java
Normal file
225
src/main/java/com/dora/controller/NoticeController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
53
src/main/java/com/dora/controller/PointsController.java
Normal file
53
src/main/java/com/dora/controller/PointsController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
163
src/main/java/com/dora/controller/UploadController.java
Normal file
163
src/main/java/com/dora/controller/UploadController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
261
src/main/java/com/dora/controller/UserController.java
Normal file
261
src/main/java/com/dora/controller/UserController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
265
src/main/java/com/dora/controller/VideoProjectController.java
Normal file
265
src/main/java/com/dora/controller/VideoProjectController.java
Normal 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));
|
||||
}
|
||||
}
|
||||
70
src/main/java/com/dora/controller/WorkController.java
Normal file
70
src/main/java/com/dora/controller/WorkController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
263
src/main/java/com/dora/controller/admin/AdminAiController.java
Normal file
263
src/main/java/com/dora/controller/admin/AdminAiController.java
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
155
src/main/java/com/dora/controller/admin/AdminUserController.java
Normal file
155
src/main/java/com/dora/controller/admin/AdminUserController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
61
src/main/java/com/dora/dto/AiModelDTO.java
Normal file
61
src/main/java/com/dora/dto/AiModelDTO.java
Normal 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;
|
||||
}
|
||||
27
src/main/java/com/dora/dto/AiProviderDTO.java
Normal file
27
src/main/java/com/dora/dto/AiProviderDTO.java
Normal 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;
|
||||
}
|
||||
19
src/main/java/com/dora/dto/AiTaskDTO.java
Normal file
19
src/main/java/com/dora/dto/AiTaskDTO.java
Normal 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;
|
||||
}
|
||||
10
src/main/java/com/dora/dto/CreatePointsOrderDTO.java
Normal file
10
src/main/java/com/dora/dto/CreatePointsOrderDTO.java
Normal 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;
|
||||
}
|
||||
27
src/main/java/com/dora/dto/PublishWorkDTO.java
Normal file
27
src/main/java/com/dora/dto/PublishWorkDTO.java
Normal 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;
|
||||
}
|
||||
13
src/main/java/com/dora/dto/RefreshTokenDTO.java
Normal file
13
src/main/java/com/dora/dto/RefreshTokenDTO.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.dora.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 刷新Token请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class RefreshTokenDTO {
|
||||
|
||||
/** 刷新令牌 */
|
||||
private String refreshToken;
|
||||
}
|
||||
18
src/main/java/com/dora/dto/UpdateProfileDTO.java
Normal file
18
src/main/java/com/dora/dto/UpdateProfileDTO.java
Normal 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;
|
||||
}
|
||||
25
src/main/java/com/dora/dto/WorkQueryDTO.java
Normal file
25
src/main/java/com/dora/dto/WorkQueryDTO.java
Normal 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;
|
||||
}
|
||||
28
src/main/java/com/dora/dto/WxLoginDTO.java
Normal file
28
src/main/java/com/dora/dto/WxLoginDTO.java
Normal 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;
|
||||
}
|
||||
43
src/main/java/com/dora/dto/admin/AdminCreateDTO.java
Normal file
43
src/main/java/com/dora/dto/admin/AdminCreateDTO.java
Normal 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;
|
||||
}
|
||||
21
src/main/java/com/dora/dto/admin/AdminLoginDTO.java
Normal file
21
src/main/java/com/dora/dto/admin/AdminLoginDTO.java
Normal 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;
|
||||
}
|
||||
24
src/main/java/com/dora/dto/admin/AdminQueryDTO.java
Normal file
24
src/main/java/com/dora/dto/admin/AdminQueryDTO.java
Normal 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;
|
||||
}
|
||||
33
src/main/java/com/dora/dto/admin/AdminRegisterDTO.java
Normal file
33
src/main/java/com/dora/dto/admin/AdminRegisterDTO.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/dora/dto/admin/AdminUpdateDTO.java
Normal file
34
src/main/java/com/dora/dto/admin/AdminUpdateDTO.java
Normal 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;
|
||||
}
|
||||
33
src/main/java/com/dora/dto/admin/AdminWorkQueryDTO.java
Normal file
33
src/main/java/com/dora/dto/admin/AdminWorkQueryDTO.java
Normal 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;
|
||||
}
|
||||
19
src/main/java/com/dora/dto/admin/EmailCodeDTO.java
Normal file
19
src/main/java/com/dora/dto/admin/EmailCodeDTO.java
Normal 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;
|
||||
}
|
||||
23
src/main/java/com/dora/dto/admin/EmailLoginDTO.java
Normal file
23
src/main/java/com/dora/dto/admin/EmailLoginDTO.java
Normal 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;
|
||||
}
|
||||
33
src/main/java/com/dora/dto/admin/OrderQueryDTO.java
Normal file
33
src/main/java/com/dora/dto/admin/OrderQueryDTO.java
Normal 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;
|
||||
}
|
||||
30
src/main/java/com/dora/dto/admin/PointsPackageDTO.java
Normal file
30
src/main/java/com/dora/dto/admin/PointsPackageDTO.java
Normal 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;
|
||||
}
|
||||
38
src/main/java/com/dora/dto/admin/RedeemCodeCreateDTO.java
Normal file
38
src/main/java/com/dora/dto/admin/RedeemCodeCreateDTO.java
Normal 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;
|
||||
}
|
||||
30
src/main/java/com/dora/dto/admin/RedeemCodeQueryDTO.java
Normal file
30
src/main/java/com/dora/dto/admin/RedeemCodeQueryDTO.java
Normal 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;
|
||||
}
|
||||
4
src/main/java/com/dora/dto/package-info.java
Normal file
4
src/main/java/com/dora/dto/package-info.java
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 数据传输对象层
|
||||
*/
|
||||
package com.dora.dto;
|
||||
15
src/main/java/com/dora/dto/video/DialogueItem.java
Normal file
15
src/main/java/com/dora/dto/video/DialogueItem.java
Normal 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;
|
||||
}
|
||||
10
src/main/java/com/dora/dto/video/GenerateScriptDTO.java
Normal file
10
src/main/java/com/dora/dto/video/GenerateScriptDTO.java
Normal 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;
|
||||
}
|
||||
18
src/main/java/com/dora/dto/video/ProjectCharacterDTO.java
Normal file
18
src/main/java/com/dora/dto/video/ProjectCharacterDTO.java
Normal 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;
|
||||
}
|
||||
13
src/main/java/com/dora/dto/video/ProjectSceneDTO.java
Normal file
13
src/main/java/com/dora/dto/video/ProjectSceneDTO.java
Normal 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;
|
||||
}
|
||||
20
src/main/java/com/dora/dto/video/SceneStoryboardDTO.java
Normal file
20
src/main/java/com/dora/dto/video/SceneStoryboardDTO.java
Normal 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;
|
||||
}
|
||||
32
src/main/java/com/dora/dto/video/ScriptGenerationResult.java
Normal file
32
src/main/java/com/dora/dto/video/ScriptGenerationResult.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/dora/dto/video/VideoProjectDTO.java
Normal file
17
src/main/java/com/dora/dto/video/VideoProjectDTO.java
Normal 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;
|
||||
}
|
||||
45
src/main/java/com/dora/entity/Admin.java
Normal file
45
src/main/java/com/dora/entity/Admin.java
Normal 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;
|
||||
}
|
||||
26
src/main/java/com/dora/entity/AdminRole.java
Normal file
26
src/main/java/com/dora/entity/AdminRole.java
Normal 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;
|
||||
}
|
||||
54
src/main/java/com/dora/entity/AiModel.java
Normal file
54
src/main/java/com/dora/entity/AiModel.java
Normal 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;
|
||||
}
|
||||
25
src/main/java/com/dora/entity/AiPromptTemplate.java
Normal file
25
src/main/java/com/dora/entity/AiPromptTemplate.java
Normal 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;
|
||||
}
|
||||
25
src/main/java/com/dora/entity/AiProvider.java
Normal file
25
src/main/java/com/dora/entity/AiProvider.java
Normal 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;
|
||||
}
|
||||
43
src/main/java/com/dora/entity/AiTask.java
Normal file
43
src/main/java/com/dora/entity/AiTask.java
Normal 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;
|
||||
}
|
||||
54
src/main/java/com/dora/entity/AiUsageRecord.java
Normal file
54
src/main/java/com/dora/entity/AiUsageRecord.java
Normal 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;
|
||||
}
|
||||
73
src/main/java/com/dora/entity/AiWork.java
Normal file
73
src/main/java/com/dora/entity/AiWork.java
Normal 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;
|
||||
}
|
||||
39
src/main/java/com/dora/entity/AiWorkComment.java
Normal file
39
src/main/java/com/dora/entity/AiWorkComment.java
Normal 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;
|
||||
}
|
||||
26
src/main/java/com/dora/entity/AiWorkLike.java
Normal file
26
src/main/java/com/dora/entity/AiWorkLike.java
Normal 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;
|
||||
}
|
||||
40
src/main/java/com/dora/entity/AuditRecord.java
Normal file
40
src/main/java/com/dora/entity/AuditRecord.java
Normal 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;
|
||||
}
|
||||
49
src/main/java/com/dora/entity/Banner.java
Normal file
49
src/main/java/com/dora/entity/Banner.java
Normal 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;
|
||||
}
|
||||
29
src/main/java/com/dora/entity/CharacterTemplate.java
Normal file
29
src/main/java/com/dora/entity/CharacterTemplate.java
Normal 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;
|
||||
}
|
||||
51
src/main/java/com/dora/entity/Notice.java
Normal file
51
src/main/java/com/dora/entity/Notice.java
Normal 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;
|
||||
}
|
||||
26
src/main/java/com/dora/entity/NoticeRead.java
Normal file
26
src/main/java/com/dora/entity/NoticeRead.java
Normal 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;
|
||||
}
|
||||
47
src/main/java/com/dora/entity/Permission.java
Normal file
47
src/main/java/com/dora/entity/Permission.java
Normal 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;
|
||||
}
|
||||
25
src/main/java/com/dora/entity/PointsOrder.java
Normal file
25
src/main/java/com/dora/entity/PointsOrder.java
Normal 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;
|
||||
}
|
||||
29
src/main/java/com/dora/entity/PointsPackage.java
Normal file
29
src/main/java/com/dora/entity/PointsPackage.java
Normal 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;
|
||||
}
|
||||
20
src/main/java/com/dora/entity/PointsRecord.java
Normal file
20
src/main/java/com/dora/entity/PointsRecord.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/dora/entity/ProjectCharacter.java
Normal file
34
src/main/java/com/dora/entity/ProjectCharacter.java
Normal 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;
|
||||
}
|
||||
24
src/main/java/com/dora/entity/ProjectScene.java
Normal file
24
src/main/java/com/dora/entity/ProjectScene.java
Normal 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;
|
||||
}
|
||||
35
src/main/java/com/dora/entity/PromotionConfig.java
Normal file
35
src/main/java/com/dora/entity/PromotionConfig.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/dora/entity/PromotionRecord.java
Normal file
34
src/main/java/com/dora/entity/PromotionRecord.java
Normal 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;
|
||||
}
|
||||
63
src/main/java/com/dora/entity/RedeemCode.java
Normal file
63
src/main/java/com/dora/entity/RedeemCode.java
Normal 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;
|
||||
}
|
||||
40
src/main/java/com/dora/entity/RedeemCodeRecord.java
Normal file
40
src/main/java/com/dora/entity/RedeemCodeRecord.java
Normal 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;
|
||||
}
|
||||
39
src/main/java/com/dora/entity/RewardMessage.java
Normal file
39
src/main/java/com/dora/entity/RewardMessage.java
Normal 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
Reference in New Issue
Block a user