正在显示
87 个修改的文件
包含
4410 行增加
和
0 行删除
README.md
0 → 100644
| 1 | +# AIGEO - AI内容生成平台 | ||
| 2 | + | ||
| 3 | +AIGEO是一个基于人工智能技术的内容生成平台,旨在帮助企业用户高效生成各种类型的内容,包括文章、落地页和网站。 | ||
| 4 | + | ||
| 5 | +## 功能特性 | ||
| 6 | + | ||
| 7 | +- **AI文章生成** - 基于关键词和主题自动生成高质量文章 | ||
| 8 | +- **AI落地页生成** - 根据业务需求自动生成营销落地页 | ||
| 9 | +- **AI网站生成** - 一键生成企业官网或电商网站 | ||
| 10 | +- **多租户架构** - 支持多企业用户独立使用 | ||
| 11 | +- **权限管理** - 完善的用户角色和权限控制 | ||
| 12 | +- **订阅计费** - 灵活的订阅计划和计费系统 | ||
| 13 | +- **内容发布** - 支持将生成内容发布到多种平台 | ||
| 14 | +- **SEO优化** - 自动生成SEO友好的内容结构 | ||
| 15 | + | ||
| 16 | +## 技术栈 | ||
| 17 | + | ||
| 18 | +- **后端**: Spring Boot 3.3.3 | ||
| 19 | +- **数据库**: MySQL 8.0 | ||
| 20 | +- **ORM框架**: JPA/Hibernate | ||
| 21 | +- **安全框架**: Spring Security + JWT | ||
| 22 | +- **缓存**: Redis | ||
| 23 | +- **API文档**: Knife4j (OpenAPI 3) | ||
| 24 | +- **任务调度**: Quartz | ||
| 25 | +- **构建工具**: Maven | ||
| 26 | +- **其他**: Lombok, Hutool, FastJSON2 | ||
| 27 | + | ||
| 28 | +## 系统要求 | ||
| 29 | + | ||
| 30 | +- JDK 17+ | ||
| 31 | +- MySQL 8.0+ | ||
| 32 | +- Redis | ||
| 33 | +- Maven 3.8+ | ||
| 34 | + | ||
| 35 | +## 快速开始 | ||
| 36 | + | ||
| 37 | +### 1. 克隆项目 | ||
| 38 | + | ||
| 39 | +```bash | ||
| 40 | +git clone <repository-url> | ||
| 41 | +cd aigeo | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +### 2. 数据库配置 | ||
| 45 | + | ||
| 46 | +在 `src/main/resources/application.yml` 中配置数据库连接: | ||
| 47 | + | ||
| 48 | +```yaml | ||
| 49 | +spring: | ||
| 50 | + datasource: | ||
| 51 | + url: jdbc:mysql://localhost:3306/aigeo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai | ||
| 52 | + username: root | ||
| 53 | + password: your_password | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +### 3. Redis配置 | ||
| 57 | + | ||
| 58 | +在 `src/main/resources/application.yml` 中配置Redis连接: | ||
| 59 | + | ||
| 60 | +```yaml | ||
| 61 | +spring: | ||
| 62 | + data: | ||
| 63 | + redis: | ||
| 64 | + host: localhost | ||
| 65 | + port: 6379 | ||
| 66 | +``` | ||
| 67 | + | ||
| 68 | +### 4. 构建和运行 | ||
| 69 | + | ||
| 70 | +```bash | ||
| 71 | +# 构建项目 | ||
| 72 | +mvn clean package | ||
| 73 | + | ||
| 74 | +# 运行项目 | ||
| 75 | +mvn spring-boot:run | ||
| 76 | + | ||
| 77 | +# 或者运行打包后的JAR文件 | ||
| 78 | +java -jar target/aigeo-1.0.0.jar | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +## API文档 | ||
| 82 | + | ||
| 83 | +项目启动后,访问以下地址查看API文档: | ||
| 84 | + | ||
| 85 | +- Knife4j UI: http://localhost:8080/api/doc.html | ||
| 86 | +- OpenAPI JSON: http://localhost:8080/api/v3/api-docs | ||
| 87 | + | ||
| 88 | +## 项目结构 | ||
| 89 | + | ||
| 90 | +``` | ||
| 91 | +src/main/java/com/aigeo | ||
| 92 | +├── AigeoApplication.java # 应用启动类 | ||
| 93 | +├── ai/ # AI配置模块 | ||
| 94 | +│ ├── controller/ | ||
| 95 | +│ ├── dto/ | ||
| 96 | +│ ├── entity/ | ||
| 97 | +│ ├── repository/ | ||
| 98 | +│ └── service/ | ||
| 99 | +├── article/ # AI文章生成模块 | ||
| 100 | +│ ├── controller/ | ||
| 101 | +│ ├── dto/ | ||
| 102 | +│ ├── entity/ | ||
| 103 | +│ ├── repository/ | ||
| 104 | +│ └── service/ | ||
| 105 | +├── auth/ # 认证模块 | ||
| 106 | +│ └── dto/ | ||
| 107 | +├── common/ # 公共组件 | ||
| 108 | +│ ├── enums/ # 枚举类 | ||
| 109 | +│ ├── exception/ # 异常处理 | ||
| 110 | +│ └── config/ # 配置类 | ||
| 111 | +├── company/ # 公司与用户模块 | ||
| 112 | +│ ├── controller/ | ||
| 113 | +│ ├── dto/ | ||
| 114 | +│ ├── entity/ | ||
| 115 | +│ ├── repository/ | ||
| 116 | +│ └── service/ | ||
| 117 | +├── config/ # 配置类 | ||
| 118 | +├── controller/ # 控制器层(旧结构,待迁移) | ||
| 119 | +├── entity/ # 实体类(旧结构,待迁移) | ||
| 120 | +├── exception/ # 异常处理 | ||
| 121 | +├── landingpage/ # 落地页生成模块 | ||
| 122 | +│ ├── controller/ | ||
| 123 | +│ ├── dto/ | ||
| 124 | +│ ├── entity/ | ||
| 125 | +│ ├── repository/ | ||
| 126 | +│ └── service/ | ||
| 127 | +├── repository/ # 数据访问层(旧结构,待迁移) | ||
| 128 | +├── service/ # 业务逻辑层(旧结构,待迁移) | ||
| 129 | +├── util/ # 工具类 | ||
| 130 | +└── website/ # 网站构建模块(待实现) | ||
| 131 | + | ||
| 132 | +src/main/resources/ | ||
| 133 | +├── application.yml # 应用配置 | ||
| 134 | +├── schema.sql # 数据库表结构 | ||
| 135 | +├── data.sql # 初始化数据 | ||
| 136 | +``` | ||
| 137 | + | ||
| 138 | +## 数据库设计 | ||
| 139 | + | ||
| 140 | +系统包含100+张数据表,主要模块包括: | ||
| 141 | + | ||
| 142 | +1. **核心模块** - 公司、用户、权限、订阅 | ||
| 143 | +2. **AI功能模块** - 文章生成、落地页生成、网站生成 | ||
| 144 | +3. **内容管理模块** - 关键词、话题、文章、页面 | ||
| 145 | +4. **发布系统模块** - 平台配置、发布任务、发布记录 | ||
| 146 | +5. **统计分析模块** - 使用统计、活动日志 | ||
| 147 | + | ||
| 148 | +## 开发规范 | ||
| 149 | + | ||
| 150 | +- 使用Lombok简化实体类代码 | ||
| 151 | +- 遵循RESTful API设计规范 | ||
| 152 | +- 使用JPA注解进行ORM映射 | ||
| 153 | +- 采用分层架构设计(Controller-Service-Repository) | ||
| 154 | +- 统一异常处理和响应格式 | ||
| 155 | + | ||
| 156 | +## 模块迁移计划 | ||
| 157 | + | ||
| 158 | +当前项目正在从扁平化结构迁移到模块化结构,迁移计划如下: | ||
| 159 | + | ||
| 160 | +1. **已完成**: | ||
| 161 | + - 创建模块化目录结构 | ||
| 162 | + - 创建DTO类 | ||
| 163 | + - 创建枚举类 | ||
| 164 | + | ||
| 165 | +2. **进行中**: | ||
| 166 | + - 迁移Controller类到对应模块 | ||
| 167 | + - 迁移Entity类到对应模块 | ||
| 168 | + - 迁移Repository类到对应模块 | ||
| 169 | + - 迁移Service类到对应模块 | ||
| 170 | + | ||
| 171 | +3. **待完成**: | ||
| 172 | + - 实现website模块 | ||
| 173 | + - 完善各模块间的交互 | ||
| 174 | + - 添加单元测试 | ||
| 175 | + | ||
| 176 | +## 部署 | ||
| 177 | + | ||
| 178 | +### 生产环境部署 | ||
| 179 | + | ||
| 180 | +```bash | ||
| 181 | +# 构建生产包 | ||
| 182 | +mvn clean package -Pprod | ||
| 183 | + | ||
| 184 | +# 运行 | ||
| 185 | +java -jar target/aigeo-1.0.0.jar --spring.profiles.active=prod | ||
| 186 | +``` | ||
| 187 | + | ||
| 188 | +### Docker部署(可选) | ||
| 189 | + | ||
| 190 | +```bash | ||
| 191 | +# 构建Docker镜像 | ||
| 192 | +docker build -t aigeo:latest . | ||
| 193 | + | ||
| 194 | +# 运行容器 | ||
| 195 | +docker run -d -p 8080:8080 aigeo:latest | ||
| 196 | +``` | ||
| 197 | + | ||
| 198 | +## 贡献 | ||
| 199 | + | ||
| 200 | +欢迎提交Issue和Pull Request来改进项目。 | ||
| 201 | + | ||
| 202 | +## 许可证 | ||
| 203 | + | ||
| 204 | +本项目仅供学习和参考使用。 |
logs/aigeo.log
0 → 100644
此 diff 太大无法显示。
src/aigeo_mysql8.sql
0 → 100644
| 1 | +SET NAMES utf8mb4; | ||
| 2 | +SET FOREIGN_KEY_CHECKS = 0; | ||
| 3 | + | ||
| 4 | +-- ==================================================================================================== | ||
| 5 | +-- 1) 核心:公司、用户、权限、订阅 | ||
| 6 | +-- ==================================================================================================== | ||
| 7 | + | ||
| 8 | +-- 公司表(多租户根) | ||
| 9 | +-- 存储企业客户的基本信息和订阅状态。 | ||
| 10 | +CREATE TABLE `ai_companies` ( | ||
| 11 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '公司主键ID', | ||
| 12 | + `name` VARCHAR(255) NOT NULL COMMENT '公司名称', | ||
| 13 | + `domain` VARCHAR(100) DEFAULT NULL COMMENT '公司域名,用于多租户访问', | ||
| 14 | + `status` ENUM('active','suspended','trial') DEFAULT 'trial' COMMENT '公司状态', | ||
| 15 | + `trial_expiry_date` DATE DEFAULT NULL COMMENT '试用到期日', | ||
| 16 | + `default_settings` JSON DEFAULT NULL COMMENT '企业默认设置(JSON)', | ||
| 17 | + `billing_email` VARCHAR(100) DEFAULT NULL COMMENT '账单邮箱', | ||
| 18 | + `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', | ||
| 19 | + `address` TEXT DEFAULT NULL COMMENT '公司地址', | ||
| 20 | + `logo_url` VARCHAR(255) DEFAULT NULL COMMENT '公司Logo URL', | ||
| 21 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 22 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 23 | + PRIMARY KEY (`id`), | ||
| 24 | + UNIQUE KEY `uk_companies_domain` (`domain`) | ||
| 25 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='公司表(多租户根)'; | ||
| 26 | + | ||
| 27 | +-- 用户表 | ||
| 28 | +-- 存储用户信息,并关联到所属公司。 | ||
| 29 | +CREATE TABLE `ai_users` ( | ||
| 30 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '用户主键ID', | ||
| 31 | + `company_id` INT NOT NULL COMMENT '所属公司ID(外键)', | ||
| 32 | + `username` VARCHAR(50) NOT NULL COMMENT '登录用户名', | ||
| 33 | + `email` VARCHAR(100) NOT NULL COMMENT '用户邮箱', | ||
| 34 | + `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希', | ||
| 35 | + `full_name` VARCHAR(100) DEFAULT NULL COMMENT '用户全名', | ||
| 36 | + `avatar_url` VARCHAR(255) DEFAULT NULL COMMENT '用户头像URL', | ||
| 37 | + `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', | ||
| 38 | + `role` ENUM('admin','manager','editor','viewer') DEFAULT 'editor' COMMENT '用户角色', | ||
| 39 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用(1启用/0禁用)', | ||
| 40 | + `last_login` TIMESTAMP NULL DEFAULT NULL COMMENT '最近登录时间', | ||
| 41 | + `last_password_change` TIMESTAMP NULL DEFAULT NULL COMMENT '上次修改密码时间', | ||
| 42 | + `failed_login_attempts` INT DEFAULT 0 COMMENT '登录失败次数', | ||
| 43 | + `locked_until` TIMESTAMP NULL DEFAULT NULL COMMENT '锁定截止时间', | ||
| 44 | + `timezone` VARCHAR(50) DEFAULT 'Asia/Shanghai' COMMENT '用户时区', | ||
| 45 | + `preferences` JSON DEFAULT NULL COMMENT '用户个性化设置', | ||
| 46 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 47 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 48 | + PRIMARY KEY (`id`), | ||
| 49 | + UNIQUE KEY `uk_users_username` (`username`), | ||
| 50 | + UNIQUE KEY `uk_users_email` (`email`), | ||
| 51 | + KEY `idx_users_company` (`company_id`), | ||
| 52 | + KEY `idx_users_company_role` (`company_id`, `role`), | ||
| 53 | + KEY `idx_users_active` (`is_active`), | ||
| 54 | + CONSTRAINT `fk_user_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE | ||
| 55 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; | ||
| 56 | + | ||
| 57 | +-- 角色表 (增强权限控制) | ||
| 58 | +-- 允许公司自定义角色及其权限。 | ||
| 59 | +CREATE TABLE `ai_roles` ( | ||
| 60 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 61 | + `company_id` INT NOT NULL COMMENT '所属公司', | ||
| 62 | + `name` VARCHAR(50) NOT NULL COMMENT '角色名称(如 admin, editor)', | ||
| 63 | + `description` VARCHAR(255) DEFAULT NULL, | ||
| 64 | + `permissions` JSON DEFAULT NULL COMMENT '该角色拥有的权限列表(JSON)', | ||
| 65 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 66 | + PRIMARY KEY (`id`), | ||
| 67 | + UNIQUE KEY `uk_roles_company_name` (`company_id`, `name`), | ||
| 68 | + CONSTRAINT `fk_role_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE | ||
| 69 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||
| 70 | + | ||
| 71 | +-- 用户-角色关联表 (支持多角色) | ||
| 72 | +-- 一个用户可以拥有多个角色。 | ||
| 73 | +CREATE TABLE `ai_user_roles` ( | ||
| 74 | + `user_id` INT NOT NULL, | ||
| 75 | + `role_id` INT NOT NULL, | ||
| 76 | + PRIMARY KEY (`user_id`, `role_id`), | ||
| 77 | + FOREIGN KEY (user_id) REFERENCES ai_users(id) ON DELETE CASCADE, | ||
| 78 | + FOREIGN KEY (role_id) REFERENCES ai_roles(id) ON DELETE CASCADE | ||
| 79 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||
| 80 | + | ||
| 81 | +-- 审计日志表 | ||
| 82 | +-- 记录用户的关键操作,用于安全审计和追踪。 | ||
| 83 | +CREATE TABLE `ai_audit_logs` ( | ||
| 84 | + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '审计日志主键ID', | ||
| 85 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 86 | + `user_id` INT NOT NULL COMMENT '操作用户ID(外键)', | ||
| 87 | + `action` VARCHAR(100) NOT NULL COMMENT '操作类型(create/update/delete/publish等)', | ||
| 88 | + `target_table` VARCHAR(100) NOT NULL COMMENT '被操作表名', | ||
| 89 | + `target_id` INT NOT NULL COMMENT '被操作记录ID', | ||
| 90 | + `details` JSON DEFAULT NULL COMMENT '操作详情(JSON,可记录前后值)', | ||
| 91 | + `ip_address` VARCHAR(45) DEFAULT NULL COMMENT '客户端IP', | ||
| 92 | + `user_agent` TEXT DEFAULT NULL COMMENT '浏览器信息', | ||
| 93 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', | ||
| 94 | + PRIMARY KEY (`id`), | ||
| 95 | + KEY `idx_audit_company` (`company_id`), | ||
| 96 | + KEY `idx_audit_user` (`user_id`), | ||
| 97 | + CONSTRAINT `fk_audit_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 98 | + CONSTRAINT `fk_audit_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`) | ||
| 99 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='操作审计日志'; | ||
| 100 | + | ||
| 101 | +-- ==================================================================================================== | ||
| 102 | +-- 2) 订阅与计费系统 | ||
| 103 | +-- ==================================================================================================== | ||
| 104 | + | ||
| 105 | +-- 订阅计划定义表 | ||
| 106 | +-- 定义可用的订阅计划及其基本属性。 | ||
| 107 | +CREATE TABLE `ai_subscription_plans` ( | ||
| 108 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 109 | + `plan_key` VARCHAR(50) NOT NULL COMMENT '计划标识符(如 free, basic, premium)', | ||
| 110 | + `name` VARCHAR(100) NOT NULL COMMENT '计划显示名称', | ||
| 111 | + `description` TEXT DEFAULT NULL COMMENT '计划描述', | ||
| 112 | + `price_monthly` DECIMAL(10,2) DEFAULT 0.00 COMMENT '月费价格', | ||
| 113 | + `price_yearly` DECIMAL(10,2) DEFAULT 0.00 COMMENT '年费价格', | ||
| 114 | + `max_users` INT DEFAULT 1 COMMENT '最大用户数', | ||
| 115 | + `max_storage_mb` INT DEFAULT 100 COMMENT '最大存储空间(MB)', | ||
| 116 | + `max_api_calls_per_day` INT DEFAULT 1000 COMMENT '每日API调用限制', | ||
| 117 | + `features` JSON DEFAULT NULL COMMENT '包含的功能列表(JSON)', | ||
| 118 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 119 | + `sort_order` INT DEFAULT 0 COMMENT '排序权重', | ||
| 120 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 121 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||
| 122 | + PRIMARY KEY (`id`), | ||
| 123 | + UNIQUE KEY `uk_plans_plan_key` (`plan_key`), | ||
| 124 | + KEY `idx_plans_active` (`is_active`) | ||
| 125 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅计划定义表'; | ||
| 126 | + | ||
| 127 | +-- 公司订阅记录表 | ||
| 128 | +-- 记录每个公司的订阅历史和当前状态。 | ||
| 129 | +CREATE TABLE `ai_company_subscriptions` ( | ||
| 130 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 131 | + `company_id` INT NOT NULL COMMENT '公司ID', | ||
| 132 | + `plan_id` INT NOT NULL COMMENT '订阅计划ID', | ||
| 133 | + `plan_key` VARCHAR(50) NOT NULL COMMENT '冗余字段:计划标识符', | ||
| 134 | + `subscription_type` ENUM('monthly','yearly') DEFAULT 'monthly' COMMENT '订阅类型', | ||
| 135 | + `status` ENUM('active','cancelled','expired','suspended') DEFAULT 'active' COMMENT '订阅状态', | ||
| 136 | + `start_date` DATE NOT NULL COMMENT '订阅开始日期', | ||
| 137 | + `end_date` DATE DEFAULT NULL COMMENT '订阅结束日期', | ||
| 138 | + `next_billing_date` DATE DEFAULT NULL COMMENT '下次计费日期', | ||
| 139 | + `trial_start_date` DATE DEFAULT NULL COMMENT '试用开始日期', | ||
| 140 | + `trial_end_date` DATE DEFAULT NULL COMMENT '试用结束日期', | ||
| 141 | + `amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '订阅金额', | ||
| 142 | + `payment_method` VARCHAR(50) DEFAULT NULL COMMENT '支付方式', | ||
| 143 | + `payment_status` ENUM('pending','paid','failed','refunded') DEFAULT 'pending' COMMENT '支付状态', | ||
| 144 | + `auto_renew` TINYINT(1) DEFAULT 1 COMMENT '是否自动续费', | ||
| 145 | + `cancel_reason` TEXT DEFAULT NULL COMMENT '取消原因', | ||
| 146 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 147 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||
| 148 | + PRIMARY KEY (`id`), | ||
| 149 | + KEY `idx_subscriptions_company` (`company_id`), | ||
| 150 | + KEY `idx_subscriptions_status` (`status`), | ||
| 151 | + KEY `idx_subscriptions_next_billing` (`next_billing_date`), | ||
| 152 | + CONSTRAINT `fk_subscription_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE, | ||
| 153 | + CONSTRAINT `fk_subscription_plan` FOREIGN KEY (`plan_id`) REFERENCES `ai_subscription_plans` (`id`) | ||
| 154 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司订阅记录表'; | ||
| 155 | + | ||
| 156 | +-- 订阅支付记录表 | ||
| 157 | +-- 记录具体的支付交易。 | ||
| 158 | +CREATE TABLE `ai_subscription_payments` ( | ||
| 159 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 160 | + `subscription_id` INT NOT NULL COMMENT '订阅ID', | ||
| 161 | + `company_id` INT NOT NULL COMMENT '公司ID', | ||
| 162 | + `amount` DECIMAL(10,2) NOT NULL COMMENT '支付金额', | ||
| 163 | + `currency` VARCHAR(3) DEFAULT 'CNY' COMMENT '货币类型', | ||
| 164 | + `payment_method` VARCHAR(50) DEFAULT NULL COMMENT '支付方式(如 alipay, wechat, stripe)', | ||
| 165 | + `transaction_id` VARCHAR(255) DEFAULT NULL COMMENT '交易ID', | ||
| 166 | + `payment_status` ENUM('pending','success','failed','refunded') DEFAULT 'pending' COMMENT '支付状态', | ||
| 167 | + `payment_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '支付时间', | ||
| 168 | + `period_start` DATE DEFAULT NULL COMMENT '计费周期开始日期', | ||
| 169 | + `period_end` DATE DEFAULT NULL COMMENT '计费周期结束日期', | ||
| 170 | + `invoice_url` VARCHAR(500) DEFAULT NULL COMMENT '发票URL', | ||
| 171 | + `failure_reason` TEXT DEFAULT NULL COMMENT '失败原因', | ||
| 172 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 173 | + PRIMARY KEY (`id`), | ||
| 174 | + KEY `idx_payments_company` (`company_id`), | ||
| 175 | + KEY `idx_payments_subscription` (`subscription_id`), | ||
| 176 | + KEY `idx_payments_status` (`payment_status`), | ||
| 177 | + CONSTRAINT `fk_payment_subscription` FOREIGN KEY (`subscription_id`) REFERENCES `ai_company_subscriptions` (`id`), | ||
| 178 | + CONSTRAINT `fk_payment_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) | ||
| 179 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅支付记录表'; | ||
| 180 | + | ||
| 181 | +-- ==================================================================================================== | ||
| 182 | +-- 3) AI功能模块与权限控制 | ||
| 183 | +-- ==================================================================================================== | ||
| 184 | + | ||
| 185 | +-- AI功能模块定义表 | ||
| 186 | +-- 定义系统提供的所有AI功能模块。 | ||
| 187 | +CREATE TABLE `ai_features` ( | ||
| 188 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 189 | + `feature_key` VARCHAR(50) NOT NULL COMMENT '功能标识符 (如 ai_copywriting, ai_landing_page)', | ||
| 190 | + `name` VARCHAR(100) NOT NULL COMMENT '功能名称', | ||
| 191 | + `description` TEXT DEFAULT NULL COMMENT '功能描述', | ||
| 192 | + `category` VARCHAR(50) DEFAULT NULL COMMENT '功能分类(如 content, marketing, website)', | ||
| 193 | + `is_premium` TINYINT(1) DEFAULT 0 COMMENT '是否为高级功能', | ||
| 194 | + `sort_order` INT DEFAULT 0 COMMENT '排序权重', | ||
| 195 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 196 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 197 | + PRIMARY KEY (`id`), | ||
| 198 | + UNIQUE KEY `uk_features_key` (`feature_key`), | ||
| 199 | + KEY `idx_features_category` (`category`), | ||
| 200 | + KEY `idx_features_active` (`is_active`) | ||
| 201 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI功能模块定义表'; | ||
| 202 | + | ||
| 203 | +-- 订阅计划功能权限表 | ||
| 204 | +-- 定义每个订阅计划对各AI功能的访问权限和使用限制。 | ||
| 205 | +CREATE TABLE `ai_plan_features` ( | ||
| 206 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 207 | + `plan_id` INT NOT NULL COMMENT '订阅计划ID', | ||
| 208 | + `feature_id` INT NOT NULL COMMENT '功能ID', | ||
| 209 | + `is_allowed` TINYINT(1) DEFAULT 1 COMMENT '是否允许使用', | ||
| 210 | + `usage_limit` INT DEFAULT NULL COMMENT '使用限制(如每月次数,NULL为无限制)', | ||
| 211 | + `limit_period` ENUM('daily','monthly','yearly','total') DEFAULT 'monthly' COMMENT '限制周期', | ||
| 212 | + `custom_config` JSON DEFAULT NULL COMMENT '自定义配置(JSON)', | ||
| 213 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 214 | + PRIMARY KEY (`id`), | ||
| 215 | + UNIQUE KEY `uk_plan_feature` (`plan_id`, `feature_id`), | ||
| 216 | + CONSTRAINT `fk_planfeature_plan` FOREIGN KEY (`plan_id`) REFERENCES `ai_subscription_plans` (`id`) ON DELETE CASCADE, | ||
| 217 | + CONSTRAINT `fk_planfeature_feature` FOREIGN KEY (`feature_id`) REFERENCES `ai_features` (`id`) ON DELETE CASCADE | ||
| 218 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅计划功能权限表'; | ||
| 219 | + | ||
| 220 | +-- 功能使用记录表 | ||
| 221 | +-- 记录用户对公司功能的具体使用情况,用于计费和分析。 | ||
| 222 | +CREATE TABLE `ai_feature_usage` ( | ||
| 223 | + `id` BIGINT NOT NULL AUTO_INCREMENT, | ||
| 224 | + `company_id` INT NOT NULL COMMENT '公司ID', | ||
| 225 | + `user_id` INT DEFAULT NULL COMMENT '用户ID(可选)', | ||
| 226 | + `feature_id` INT NOT NULL COMMENT '功能ID', | ||
| 227 | + `usage_type` VARCHAR(50) DEFAULT NULL COMMENT '使用类型(如 generate, export, analyze)', | ||
| 228 | + `usage_count` INT DEFAULT 1 COMMENT '使用次数', | ||
| 229 | + `related_resource_id` VARCHAR(100) DEFAULT NULL COMMENT '相关资源ID(如文章ID、页面ID)', | ||
| 230 | + `ip_address` VARCHAR(45) DEFAULT NULL COMMENT '客户端IP', | ||
| 231 | + `user_agent` TEXT DEFAULT NULL COMMENT '用户代理', | ||
| 232 | + `metadata` JSON DEFAULT NULL COMMENT '额外元数据', | ||
| 233 | + `used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '使用时间', | ||
| 234 | + PRIMARY KEY (`id`), | ||
| 235 | + KEY `idx_usage_company_feature` (`company_id`, `feature_id`), | ||
| 236 | + KEY `idx_usage_feature_date` (`feature_id`, `used_at`), | ||
| 237 | + KEY `idx_usage_user` (`user_id`), | ||
| 238 | + CONSTRAINT `fk_usage_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE, | ||
| 239 | + CONSTRAINT `fk_usage_feature` FOREIGN KEY (`feature_id`) REFERENCES `ai_features` (`id`) | ||
| 240 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='功能使用记录表'; | ||
| 241 | + | ||
| 242 | +-- 功能使用统计表(用于快速查询) | ||
| 243 | +-- 按日统计功能使用量,提高查询效率。 | ||
| 244 | +CREATE TABLE `ai_feature_usage_stats` ( | ||
| 245 | + `id` INT NOT NULL AUTO_INCREMENT, | ||
| 246 | + `company_id` INT NOT NULL COMMENT '公司ID', | ||
| 247 | + `feature_id` INT NOT NULL COMMENT '功能ID', | ||
| 248 | + `stat_date` DATE NOT NULL COMMENT '统计日期', | ||
| 249 | + `usage_count` INT DEFAULT 0 COMMENT '当日使用次数', | ||
| 250 | + `last_used_at` TIMESTAMP DEFAULT NULL COMMENT '最后使用时间', | ||
| 251 | + PRIMARY KEY (`id`), | ||
| 252 | + UNIQUE KEY `uk_stats_company_feature_date` (`company_id`, `feature_id`, `stat_date`), | ||
| 253 | + KEY `idx_stats_company` (`company_id`), | ||
| 254 | + KEY `idx_stats_feature` (`feature_id`), | ||
| 255 | + CONSTRAINT `fk_stats_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE, | ||
| 256 | + CONSTRAINT `fk_stats_feature` FOREIGN KEY (`feature_id`) REFERENCES `ai_features` (`id`) ON DELETE CASCADE | ||
| 257 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='功能使用统计表'; | ||
| 258 | + | ||
| 259 | +-- ==================================================================================================== | ||
| 260 | +-- 4) AI内容生成核心:模型、Prompt、文件 | ||
| 261 | +-- ==================================================================================================== | ||
| 262 | + | ||
| 263 | +-- AI服务配置表 | ||
| 264 | +-- 存储连接到不同AI服务(如Dify, OpenAI)的配置信息。 | ||
| 265 | +CREATE TABLE `ai_dify_api_configs` ( | ||
| 266 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'AI配置主键ID', | ||
| 267 | + `company_id` INT DEFAULT NULL COMMENT '公司ID(NULL 表示共享/通用)', | ||
| 268 | + `provider` ENUM('dify','openai','anthropic','google','azure_openai','other') DEFAULT 'dify' COMMENT 'AI 提供方', | ||
| 269 | + `name` VARCHAR(100) NOT NULL COMMENT '配置名称(便于识别)', | ||
| 270 | + `base_url` VARCHAR(255) DEFAULT NULL COMMENT 'API 基础地址(可选)', | ||
| 271 | + `api_key` VARCHAR(255) DEFAULT NULL COMMENT 'API Key/Token', | ||
| 272 | + `model_name` VARCHAR(100) DEFAULT NULL COMMENT '模型名称', | ||
| 273 | + `temperature` DECIMAL(3,2) DEFAULT 0.70 COMMENT '默认温度值', | ||
| 274 | + `top_p` DECIMAL(3,2) DEFAULT 1.00 COMMENT 'TopP 值', | ||
| 275 | + `max_tokens` INT DEFAULT 2048 COMMENT '最大生成 token 数', | ||
| 276 | + `request_headers` JSON DEFAULT NULL COMMENT '额外请求头(JSON)', | ||
| 277 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 278 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 279 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 280 | + PRIMARY KEY (`id`), | ||
| 281 | + KEY `idx_dify_company` (`company_id`) | ||
| 282 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI 服务配置(Dify/OpenAI 等)'; | ||
| 283 | + | ||
| 284 | +-- Prompt 模板表 | ||
| 285 | +-- 存储可复用的Prompt模板。 | ||
| 286 | +CREATE TABLE `ai_prompt_templates` ( | ||
| 287 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Prompt 模板主键ID', | ||
| 288 | + `company_id` INT DEFAULT NULL COMMENT '公司ID(NULL 表示系统模板)', | ||
| 289 | + `name` VARCHAR(100) NOT NULL COMMENT '模板名称', | ||
| 290 | + `description` VARCHAR(255) DEFAULT NULL COMMENT '模板描述', | ||
| 291 | + `language` VARCHAR(20) DEFAULT 'zh' COMMENT '默认语言编码(en/zh 等)', | ||
| 292 | + `content` LONGTEXT NOT NULL COMMENT '模板内容(可含变量占位符)', | ||
| 293 | + `variables` JSON DEFAULT NULL COMMENT '变量说明(JSON)', | ||
| 294 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 295 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 296 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 297 | + PRIMARY KEY (`id`), | ||
| 298 | + KEY `idx_prompt_company` (`company_id`) | ||
| 299 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Prompt 模板表'; | ||
| 300 | + | ||
| 301 | +-- 上传文件表 | ||
| 302 | +-- 管理用户上传的文件,如知识库、图片、视频等。 | ||
| 303 | +CREATE TABLE `ai_uploaded_files` ( | ||
| 304 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '上传文件主键ID', | ||
| 305 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 306 | + `user_id` INT NOT NULL COMMENT '上传者用户ID(外键)', | ||
| 307 | + `file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名', | ||
| 308 | + `file_path` VARCHAR(500) NOT NULL COMMENT '服务器存储路径或外部URL', | ||
| 309 | + `file_type` ENUM('knowledge','image','video','document','other') NOT NULL COMMENT '文件类型', | ||
| 310 | + `file_size` BIGINT DEFAULT 0 COMMENT '文件大小(字节)', | ||
| 311 | + `mime_type` VARCHAR(100) DEFAULT NULL COMMENT 'MIME 类型', | ||
| 312 | + `checksum` VARCHAR(64) DEFAULT NULL COMMENT '校验和(可选)', | ||
| 313 | + `version` INT DEFAULT 1 COMMENT '版本号(用于版本控制)', | ||
| 314 | + `status` ENUM('active','archived','deleted') DEFAULT 'active' COMMENT '文件状态', | ||
| 315 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', | ||
| 316 | + PRIMARY KEY (`id`), | ||
| 317 | + KEY `idx_files_company` (`company_id`), | ||
| 318 | + KEY `idx_files_user` (`user_id`), | ||
| 319 | + CONSTRAINT `fk_file_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 320 | + CONSTRAINT `fk_file_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`) | ||
| 321 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='上传文件表(知识库/媒体等)'; | ||
| 322 | + | ||
| 323 | +-- ==================================================================================================== | ||
| 324 | +-- 5) AI内容生成:文章 | ||
| 325 | +-- ==================================================================================================== | ||
| 326 | + | ||
| 327 | +-- 文章类型表 | ||
| 328 | +-- 分类文章,如产品介绍、新闻稿等。 | ||
| 329 | +CREATE TABLE `ai_article_types` ( | ||
| 330 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章类型主键ID', | ||
| 331 | + `name` VARCHAR(50) NOT NULL COMMENT '文章类型名称(如产品介绍)', | ||
| 332 | + `description` VARCHAR(255) DEFAULT NULL COMMENT '类型说明', | ||
| 333 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 334 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 335 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 336 | + PRIMARY KEY (`id`), | ||
| 337 | + UNIQUE KEY `uk_article_types_name` (`name`) | ||
| 338 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章类型表'; | ||
| 339 | + | ||
| 340 | +-- 文章生成配置表 | ||
| 341 | +-- 存储文章生成的参数和偏好设置。 | ||
| 342 | +CREATE TABLE `ai_article_generation_configs` ( | ||
| 343 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章生成配置主键ID', | ||
| 344 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 345 | + `name` VARCHAR(100) NOT NULL COMMENT '配置名称', | ||
| 346 | + `platform_id` INT DEFAULT NULL COMMENT '适用发布平台ID(可选)', | ||
| 347 | + `article_type_id` INT DEFAULT NULL COMMENT '文章类型ID(外键)', | ||
| 348 | + `writing_language` VARCHAR(20) DEFAULT 'en' COMMENT '写作语言(en/zh)', | ||
| 349 | + `remove_ai_tone` TINYINT(1) DEFAULT 1 COMMENT '是否去除AI味道', | ||
| 350 | + `ai_taste_level` ENUM('colloquial','junior_high','senior_high','professional') DEFAULT 'junior_high' COMMENT 'AI风格等级', | ||
| 351 | + `article_length_min` INT DEFAULT 800 COMMENT '最小长度(字符)', | ||
| 352 | + `article_length_max` INT DEFAULT 1500 COMMENT '最大长度(字符)', | ||
| 353 | + `auto_seo_optimization` TINYINT(1) DEFAULT 1 COMMENT '自动SEO优化', | ||
| 354 | + `keyword_density_min` DECIMAL(5,2) DEFAULT 1.00 COMMENT '关键词密度下限(%)', | ||
| 355 | + `keyword_density_max` DECIMAL(5,2) DEFAULT 2.00 COMMENT '关键词密度上限(%)', | ||
| 356 | + `auto_geo_optimization` TINYINT(1) DEFAULT 1 COMMENT '是否自动GEO优化', | ||
| 357 | + `auto_structured_data` TINYINT(1) DEFAULT 1 COMMENT '是否自动生成结构化数据', | ||
| 358 | + `multi_version_count` INT DEFAULT 3 COMMENT '生成版本数量', | ||
| 359 | + `extra_options` JSON DEFAULT NULL COMMENT '额外选项(JSON)', | ||
| 360 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 361 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 362 | + PRIMARY KEY (`id`), | ||
| 363 | + KEY `idx_config_company` (`company_id`), | ||
| 364 | + KEY `idx_config_platform` (`platform_id`), | ||
| 365 | + KEY `idx_config_article_type` (`article_type_id`), | ||
| 366 | + CONSTRAINT `fk_config_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 367 | + CONSTRAINT `fk_config_platform` FOREIGN KEY (`platform_id`) REFERENCES `ai_publishing_platforms` (`id`), | ||
| 368 | + CONSTRAINT `fk_config_article_type` FOREIGN KEY (`article_type_id`) REFERENCES `ai_article_types` (`id`) | ||
| 369 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI 文章生成配置'; | ||
| 370 | + | ||
| 371 | +-- 文章生成任务表 | ||
| 372 | +-- 记录每次文章生成的请求和状态。 | ||
| 373 | +CREATE TABLE `ai_article_generation_tasks` ( | ||
| 374 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章生成任务主键ID', | ||
| 375 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 376 | + `user_id` INT NOT NULL COMMENT '发起用户ID(外键)', | ||
| 377 | + `config_id` INT DEFAULT NULL COMMENT '使用的生成配置ID(外键)', | ||
| 378 | + `article_theme` VARCHAR(255) DEFAULT NULL COMMENT '文章主题/标题(输入)', | ||
| 379 | + `topic_ids` TEXT DEFAULT NULL COMMENT '所选话题ID列表(逗号分隔)', | ||
| 380 | + `reference_urls` TEXT DEFAULT NULL COMMENT '参考URL列表(逗号分隔)', | ||
| 381 | + `reference_content` LONGTEXT DEFAULT NULL COMMENT '高度参考链接抓取到的内容摘要', | ||
| 382 | + `status` ENUM('pending','processing','completed','failed') DEFAULT 'pending' COMMENT '任务状态', | ||
| 383 | + `progress` TINYINT DEFAULT 0 COMMENT '进度(0-100)', | ||
| 384 | + `error_message` TEXT DEFAULT NULL COMMENT '错误信息', | ||
| 385 | + `dify_api_config_id` INT DEFAULT NULL COMMENT '调用的AI配置ID(外键)', | ||
| 386 | + `prompt_template_id` INT DEFAULT NULL COMMENT '使用的 Prompt 模板 ID(外键)', | ||
| 387 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 388 | + `completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间', | ||
| 389 | + PRIMARY KEY (`id`), | ||
| 390 | + KEY `idx_task_company_status` (`company_id`,`status`), | ||
| 391 | + KEY `idx_task_user` (`user_id`), | ||
| 392 | + KEY `idx_task_config` (`config_id`), | ||
| 393 | + CONSTRAINT `fk_task_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 394 | + CONSTRAINT `fk_task_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`), | ||
| 395 | + CONSTRAINT `fk_task_config` FOREIGN KEY (`config_id`) REFERENCES `ai_article_generation_configs` (`id`), | ||
| 396 | + CONSTRAINT `fk_task_ai_config` FOREIGN KEY (`dify_api_config_id`) REFERENCES `ai_dify_api_configs` (`id`), | ||
| 397 | + CONSTRAINT `fk_task_prompt_template` FOREIGN KEY (`prompt_template_id`) REFERENCES `ai_prompt_templates` (`id`) | ||
| 398 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章生成任务'; | ||
| 399 | + | ||
| 400 | +-- 生成的文章表(多版本支持) | ||
| 401 | +-- 存储AI生成的最终文章内容。 | ||
| 402 | +CREATE TABLE `ai_generated_articles` ( | ||
| 403 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '生成文章主键ID', | ||
| 404 | + `task_id` INT DEFAULT NULL COMMENT '来源生成任务ID(外键,可空)', | ||
| 405 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 406 | + `version` INT DEFAULT 1 COMMENT '文章版本号(用于多版本)', | ||
| 407 | + `title` VARCHAR(255) DEFAULT NULL COMMENT '文章标题', | ||
| 408 | + `content` LONGTEXT DEFAULT NULL COMMENT '文章纯文本内容(不含HTML)', | ||
| 409 | + `html_content` LONGTEXT DEFAULT NULL COMMENT '文章HTML格式内容(含排版)', | ||
| 410 | + `faq_section` JSON DEFAULT NULL COMMENT 'FAQ 部分(JSON)', | ||
| 411 | + `structured_data` JSON DEFAULT NULL COMMENT '结构化数据 JSON-LD(全文)', | ||
| 412 | + `word_count` INT DEFAULT 0 COMMENT '文章字数统计', | ||
| 413 | + `keyword_density` JSON DEFAULT NULL COMMENT '关键词密度分析结果(JSON)', | ||
| 414 | + `is_selected` TINYINT(1) DEFAULT 0 COMMENT '是否被选为最终版本(1是)', | ||
| 415 | + `status` ENUM('draft','approved','archived','deleted') DEFAULT 'draft' COMMENT '文章状态', | ||
| 416 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 417 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 418 | + PRIMARY KEY (`id`), | ||
| 419 | + KEY `idx_articles_company_status` (`company_id`,`status`), | ||
| 420 | + KEY `idx_articles_task` (`task_id`), | ||
| 421 | + CONSTRAINT `fk_article_task` FOREIGN KEY (`task_id`) REFERENCES `ai_article_generation_tasks` (`id`), | ||
| 422 | + CONSTRAINT `fk_article_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) | ||
| 423 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='生成的文章表(多版本支持)'; | ||
| 424 | + | ||
| 425 | +-- 文章FAQ列表 | ||
| 426 | +-- 存储文章的FAQ部分。 | ||
| 427 | +CREATE TABLE `ai_article_faqs` ( | ||
| 428 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章 FAQ 主键ID', | ||
| 429 | + `article_id` INT NOT NULL COMMENT '文章ID(外键)', | ||
| 430 | + `question` VARCHAR(255) NOT NULL COMMENT 'FAQ 问题', | ||
| 431 | + `answer` TEXT NOT NULL COMMENT 'FAQ 回答', | ||
| 432 | + PRIMARY KEY (`id`), | ||
| 433 | + KEY `idx_article_faqs_article` (`article_id`), | ||
| 434 | + CONSTRAINT `fk_faq_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE | ||
| 435 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章 FAQ 列表'; | ||
| 436 | + | ||
| 437 | +-- 文章结构化数据 | ||
| 438 | +-- 存储文章的结构化数据(JSON-LD)。 | ||
| 439 | +CREATE TABLE `ai_article_structured_data` ( | ||
| 440 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '结构化数据主键ID', | ||
| 441 | + `article_id` INT NOT NULL COMMENT '文章ID(外键)', | ||
| 442 | + `json_ld` JSON NOT NULL COMMENT 'JSON-LD 结构化数据内容', | ||
| 443 | + PRIMARY KEY (`id`), | ||
| 444 | + KEY `idx_structured_article` (`article_id`), | ||
| 445 | + CONSTRAINT `fk_structured_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE | ||
| 446 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章结构化数据(单独表便于管理/更新)'; | ||
| 447 | + | ||
| 448 | +-- 文章媒体资源 | ||
| 449 | +-- 存储文章关联的图片、视频等媒体。 | ||
| 450 | +CREATE TABLE `ai_article_media` ( | ||
| 451 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章媒体资源主键ID', | ||
| 452 | + `article_id` INT NOT NULL COMMENT '文章ID(外键)', | ||
| 453 | + `media_type` ENUM('image','video','audio') NOT NULL COMMENT '媒体类型', | ||
| 454 | + `url` VARCHAR(500) NOT NULL COMMENT '媒体链接或存储路径', | ||
| 455 | + `prompt` TEXT DEFAULT NULL COMMENT '生成提示语(AI 配图时记录)', | ||
| 456 | + `source_type` ENUM('ai_generated','user_provided','external') DEFAULT 'external' COMMENT '资源来源类型', | ||
| 457 | + `alt_text` VARCHAR(255) DEFAULT NULL COMMENT '图片替代文本(SEO)', | ||
| 458 | + `caption` VARCHAR(255) DEFAULT NULL COMMENT '图片说明/标题', | ||
| 459 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 460 | + PRIMARY KEY (`id`), | ||
| 461 | + KEY `idx_media_article` (`article_id`), | ||
| 462 | + CONSTRAINT `fk_media_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE | ||
| 463 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章媒体资源(多张图片/多视频)'; | ||
| 464 | + | ||
| 465 | +-- 文章多语言翻译表 | ||
| 466 | +-- 存储文章的不同语言版本。 | ||
| 467 | +CREATE TABLE `ai_article_translations` ( | ||
| 468 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章翻译主键ID', | ||
| 469 | + `article_id` INT NOT NULL COMMENT '原文章ID(外键)', | ||
| 470 | + `language` VARCHAR(20) NOT NULL COMMENT '翻译目标语言(如 en/zh)', | ||
| 471 | + `title` VARCHAR(255) DEFAULT NULL COMMENT '译文标题', | ||
| 472 | + `content` LONGTEXT DEFAULT NULL COMMENT '译文纯文本内容', | ||
| 473 | + `html_content` LONGTEXT DEFAULT NULL COMMENT '译文 HTML 内容', | ||
| 474 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 475 | + PRIMARY KEY (`id`), | ||
| 476 | + UNIQUE KEY `uk_article_language` (`article_id`,`language`), | ||
| 477 | + KEY `idx_translations_article` (`article_id`), | ||
| 478 | + CONSTRAINT `fk_translation_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE | ||
| 479 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章多语言翻译表'; | ||
| 480 | + | ||
| 481 | +-- ==================================================================================================== | ||
| 482 | +-- 6) AI内容生成:落地页 | ||
| 483 | +-- ==================================================================================================== | ||
| 484 | + | ||
| 485 | +-- 落地页模板表 | ||
| 486 | +-- 存储可用的落地页布局模板。 | ||
| 487 | +CREATE TABLE `ai_landing_page_templates` ( | ||
| 488 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '落地页模板主键ID', | ||
| 489 | + `name` VARCHAR(100) NOT NULL COMMENT '模板名称(如:单栏布局)', | ||
| 490 | + `code` VARCHAR(50) NOT NULL COMMENT '模板代码(如:single-column)', | ||
| 491 | + `description` VARCHAR(255) DEFAULT NULL COMMENT '模板描述', | ||
| 492 | + `preview_image_url` VARCHAR(255) DEFAULT NULL COMMENT '预览图URL', | ||
| 493 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 494 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 495 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 496 | + PRIMARY KEY (`id`), | ||
| 497 | + UNIQUE KEY `uk_lp_templates_code` (`code`) | ||
| 498 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='落地页布局与设计模板'; | ||
| 499 | + | ||
| 500 | +-- 落地页项目表 | ||
| 501 | +-- 代表一个完整的落地页创建流程。 | ||
| 502 | +CREATE TABLE `ai_landing_page_projects` ( | ||
| 503 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '落地页项目主键ID', | ||
| 504 | + `company_id` INT NOT NULL COMMENT '所属公司ID(外键)', | ||
| 505 | + `user_id` INT NOT NULL COMMENT '创建者用户ID(外键)', | ||
| 506 | + `name` VARCHAR(255) NOT NULL COMMENT '落地页项目名称(用于内部识别)', | ||
| 507 | + `status` ENUM('draft','configuring','generated','published','archived') DEFAULT 'draft' COMMENT '项目状态', | ||
| 508 | + `last_step_completed` INT DEFAULT 0 COMMENT '最后完成的步骤号', | ||
| 509 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 510 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 511 | + PRIMARY KEY (`id`), | ||
| 512 | + KEY `idx_lp_projects_company` (`company_id`), | ||
| 513 | + KEY `idx_lp_projects_user` (`user_id`), | ||
| 514 | + CONSTRAINT `fk_lp_project_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 515 | + CONSTRAINT `fk_lp_project_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`) | ||
| 516 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI落地页构建项目表'; | ||
| 517 | + | ||
| 518 | +-- 落地页各步骤配置数据 | ||
| 519 | +-- 存储落地页构建过程中用户输入的所有配置信息。 | ||
| 520 | +CREATE TABLE `ai_landing_page_step_configs` ( | ||
| 521 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '配置主键ID', | ||
| 522 | + `project_id` INT NOT NULL COMMENT '落地页项目ID(外键)', | ||
| 523 | + -- Step 1: 目标用户 | ||
| 524 | + `target_audience_desc` TEXT COMMENT '目标用户描述', | ||
| 525 | + `user_pain_points` TEXT COMMENT '用户痛点(可JSON或换行分隔)', | ||
| 526 | + `user_expectations` TEXT COMMENT '用户期望结果', | ||
| 527 | + `age_groups` VARCHAR(255) DEFAULT NULL COMMENT '年龄段(逗号分隔)', | ||
| 528 | + `gender_preference` ENUM('male','female','balanced') DEFAULT 'balanced' COMMENT '性别倾向', | ||
| 529 | + `behavior_characteristics` VARCHAR(255) DEFAULT NULL COMMENT '用户行为特征(逗号分隔)', | ||
| 530 | + `decision_making_styles` VARCHAR(255) DEFAULT NULL COMMENT '用户决策方式(逗号分隔)', | ||
| 531 | + -- Step 2: 落地页目标 | ||
| 532 | + `industry_primary` VARCHAR(100) DEFAULT NULL COMMENT '一级行业', | ||
| 533 | + `industry_secondary` VARCHAR(100) DEFAULT NULL COMMENT '二级行业', | ||
| 534 | + `industry_tertiary` VARCHAR(100) DEFAULT NULL COMMENT '子分类', | ||
| 535 | + `marketing_goal` VARCHAR(50) DEFAULT NULL COMMENT '营销目标(lead-collection, product-sales等)', | ||
| 536 | + -- Step 3: 风格与配色 | ||
| 537 | + `design_style` VARCHAR(50) DEFAULT NULL COMMENT '设计风格(modern, professional等)', | ||
| 538 | + `primary_color` VARCHAR(20) DEFAULT NULL COMMENT '主色调', | ||
| 539 | + `accent_color` VARCHAR(20) DEFAULT NULL COMMENT '辅助色调', | ||
| 540 | + `template_id` INT DEFAULT NULL COMMENT '布局模板ID(外键)', | ||
| 541 | + -- Step 4: 核心卖点 | ||
| 542 | + `unique_value_proposition` TEXT COMMENT '独特价值主张', | ||
| 543 | + `core_advantages` JSON DEFAULT NULL COMMENT '核心优势列表(JSON数组)', | ||
| 544 | + `primary_keyword` VARCHAR(255) DEFAULT NULL COMMENT '主要关键词', | ||
| 545 | + `secondary_keywords` JSON DEFAULT NULL COMMENT '次要关键词列表(JSON数组)', | ||
| 546 | + -- Step 5: 页面内容 | ||
| 547 | + `content_generation_type` ENUM('ai','custom','upload') DEFAULT 'ai' COMMENT '内容生成方式', | ||
| 548 | + `company_name` VARCHAR(255) DEFAULT NULL COMMENT '公司全称', | ||
| 549 | + `brand_name` VARCHAR(255) DEFAULT NULL COMMENT '品牌名称', | ||
| 550 | + `company_description` TEXT COMMENT '公司介绍', | ||
| 551 | + `video_url` VARCHAR(500) DEFAULT NULL COMMENT '视频链接', | ||
| 552 | + `logo_file_id` INT DEFAULT NULL COMMENT '公司Logo文件ID(关联ai_uploaded_files)', | ||
| 553 | + `contact_info` JSON DEFAULT NULL COMMENT '联系信息(地址、电话、邮箱、工作时间)', | ||
| 554 | + `social_media_links` JSON DEFAULT NULL COMMENT '社交媒体链接(JSON数组)', | ||
| 555 | + -- Step 6: CTA | ||
| 556 | + `primary_cta_text` VARCHAR(100) DEFAULT NULL COMMENT '主要CTA按钮文案', | ||
| 557 | + `secondary_cta_texts` JSON DEFAULT NULL COMMENT '次要CTA按钮文案(JSON数组)', | ||
| 558 | + -- Step 7: 信任元素 | ||
| 559 | + `testimonials` JSON DEFAULT NULL COMMENT '客户评价/推荐语(JSON数组,含姓名、职位、内容)', | ||
| 560 | + `social_proofs` JSON DEFAULT NULL COMMENT '社会证明数据(JSON数组,含标签、数值)', | ||
| 561 | + -- Step 8: 表单字段 | ||
| 562 | + `form_fields` JSON DEFAULT NULL COMMENT '表单字段配置(JSON对象,key为字段名,value为是否启用)', | ||
| 563 | + -- Step 9: 生成与部署 | ||
| 564 | + `page_title` VARCHAR(255) DEFAULT NULL COMMENT '页面SEO标题', | ||
| 565 | + `page_description` TEXT COMMENT '页面SEO描述', | ||
| 566 | + `ga_tracking_code` TEXT COMMENT '谷歌广告跟踪代码', | ||
| 567 | + `deployment_method` ENUM('ftp','link') DEFAULT 'link' COMMENT '部署方式', | ||
| 568 | + `deployment_config` JSON DEFAULT NULL COMMENT '部署配置(FTP信息或子域名)', | ||
| 569 | + `pricing_plan` VARCHAR(50) DEFAULT NULL COMMENT '选择的套餐(basic, pro, enterprise)', | ||
| 570 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 571 | + PRIMARY KEY (`id`), | ||
| 572 | + UNIQUE KEY `uk_lp_config_project` (`project_id`), | ||
| 573 | + CONSTRAINT `fk_lp_config_project` FOREIGN KEY (`project_id`) REFERENCES `ai_landing_page_projects` (`id`) ON DELETE CASCADE, | ||
| 574 | + CONSTRAINT `fk_lp_config_template` FOREIGN KEY (`template_id`) REFERENCES `ai_landing_page_templates` (`id`) | ||
| 575 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='落地页各步骤配置数据'; | ||
| 576 | + | ||
| 577 | +-- 落地页AI生成任务表 | ||
| 578 | +-- 记录落地页生成的请求和状态。 | ||
| 579 | +CREATE TABLE `ai_landing_page_generation_tasks` ( | ||
| 580 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务主键ID', | ||
| 581 | + `project_id` INT NOT NULL COMMENT '落地页项目ID (外键)', | ||
| 582 | + `user_id` INT NOT NULL COMMENT '发起用户ID (外键)', | ||
| 583 | + `status` ENUM('pending','processing','completed','failed') DEFAULT 'pending' COMMENT '任务状态', | ||
| 584 | + `progress` TINYINT DEFAULT 0 COMMENT '进度 (0-100)', | ||
| 585 | + `dify_api_config_id` INT DEFAULT NULL COMMENT '调用的AI配置ID (外键, 关联 ai_dify_api_configs)', | ||
| 586 | + `prompt_template_id` INT DEFAULT NULL COMMENT '使用的Prompt模板ID (外键, 关联 ai_prompt_templates)', | ||
| 587 | + `final_prompt_snapshot` LONGTEXT DEFAULT NULL COMMENT '发送给AI的最终Prompt快照(用于调试和记录)', | ||
| 588 | + `error_message` TEXT DEFAULT NULL COMMENT '错误信息', | ||
| 589 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 590 | + `completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间', | ||
| 591 | + PRIMARY KEY (`id`), | ||
| 592 | + KEY `idx_lp_task_project` (`project_id`), | ||
| 593 | + KEY `idx_lp_task_status` (`status`), | ||
| 594 | + CONSTRAINT `fk_lp_task_project` FOREIGN KEY (`project_id`) REFERENCES `ai_landing_page_projects` (`id`), | ||
| 595 | + CONSTRAINT `fk_lp_task_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`), | ||
| 596 | + CONSTRAINT `fk_lp_task_ai_config` FOREIGN KEY (`dify_api_config_id`) REFERENCES `ai_dify_api_configs` (`id`), | ||
| 597 | + CONSTRAINT `fk_lp_task_prompt_template` FOREIGN KEY (`prompt_template_id`) REFERENCES `ai_prompt_templates` (`id`) | ||
| 598 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='落地页AI生成任务表'; | ||
| 599 | + | ||
| 600 | +-- 最终生成的落地页(支持多版本) | ||
| 601 | +-- 存储AI生成的最终落地页HTML代码。 | ||
| 602 | +CREATE TABLE `ai_generated_landing_pages` ( | ||
| 603 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '生成落地页主键ID', | ||
| 604 | + `project_id` INT NOT NULL COMMENT '来源项目ID(外键)', | ||
| 605 | + `version_code` VARCHAR(20) NOT NULL DEFAULT 'A' COMMENT '版本标识(用于A/B测试,如A, B)', | ||
| 606 | + `html_content` LONGTEXT COMMENT '落地页HTML内容', | ||
| 607 | + `status` ENUM('draft','final','published') DEFAULT 'draft' COMMENT '版本状态', | ||
| 608 | + `publish_url` VARCHAR(500) DEFAULT NULL COMMENT '发布后的URL(使用link方式时)', | ||
| 609 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间', | ||
| 610 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 611 | + PRIMARY KEY (`id`), | ||
| 612 | + UNIQUE KEY `uk_lp_project_version` (`project_id`, `version_code`), | ||
| 613 | + CONSTRAINT `fk_lp_page_project` FOREIGN KEY (`project_id`) REFERENCES `ai_landing_page_projects` (`id`) | ||
| 614 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='最终生成的落地页(支持多版本)'; | ||
| 615 | + | ||
| 616 | +-- 落地页项目关联的媒体资产 | ||
| 617 | +-- 存储落地页项目中使用的图片、Logo等文件。 | ||
| 618 | +CREATE TABLE `ai_landing_page_assets` ( | ||
| 619 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '资产主键ID', | ||
| 620 | + `project_id` INT NOT NULL COMMENT '落地页项目ID(外键)', | ||
| 621 | + `asset_type` ENUM('product_image','certification_logo','testimonial_avatar') NOT NULL COMMENT '资产类型', | ||
| 622 | + `uploaded_file_id` INT NOT NULL COMMENT '上传文件ID(外键)', | ||
| 623 | + `related_data` JSON DEFAULT NULL COMMENT '相关数据(如关联的产品名或评价人)', | ||
| 624 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 625 | + PRIMARY KEY (`id`), | ||
| 626 | + KEY `idx_lp_assets_project` (`project_id`), | ||
| 627 | + CONSTRAINT `fk_lp_asset_project` FOREIGN KEY (`project_id`) REFERENCES `ai_landing_page_projects` (`id`), | ||
| 628 | + CONSTRAINT `fk_lp_asset_file` FOREIGN KEY (`uploaded_file_id`) REFERENCES `ai_uploaded_files` (`id`) | ||
| 629 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='落地页项目关联的媒体资产'; | ||
| 630 | + | ||
| 631 | +-- ==================================================================================================== | ||
| 632 | +-- 7) AI内容生成:网站 (简化版) | ||
| 633 | +-- ==================================================================================================== | ||
| 634 | + | ||
| 635 | +-- AI网站构建项目总表 | ||
| 636 | +-- 代表一个完整的网站创建流程。 | ||
| 637 | +CREATE TABLE `ai_website_projects` ( | ||
| 638 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '网站项目主键ID', | ||
| 639 | + `company_id` INT NOT NULL COMMENT '所属公司ID (外键)', | ||
| 640 | + `user_id` INT NOT NULL COMMENT '创建者用户ID (外键)', | ||
| 641 | + `project_name` VARCHAR(255) NOT NULL COMMENT '项目名称 (用于内部识别)', | ||
| 642 | + `site_name` VARCHAR(255) DEFAULT NULL COMMENT '最终生成的网站名称', | ||
| 643 | + `status` ENUM('draft','configuring','generating','completed','published') DEFAULT 'draft' COMMENT '项目状态', | ||
| 644 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 645 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 646 | + PRIMARY KEY (`id`), | ||
| 647 | + KEY `idx_website_projects_company` (`company_id`), | ||
| 648 | + CONSTRAINT `fk_website_project_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 649 | + CONSTRAINT `fk_website_project_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`) | ||
| 650 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI网站构建项目总表'; | ||
| 651 | + | ||
| 652 | +-- AI网站构建器配置(存储站点地图) | ||
| 653 | +-- 存储网站的全局配置和站点地图结构。 | ||
| 654 | +CREATE TABLE `ai_website_build_configs` ( | ||
| 655 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '配置主键ID', | ||
| 656 | + `project_id` INT NOT NULL COMMENT '网站项目ID (外键)', | ||
| 657 | + `website_type` VARCHAR(50) DEFAULT NULL COMMENT '网站类型 (business, portfolio等)', | ||
| 658 | + `site_identity` JSON DEFAULT NULL COMMENT '网站身份信息 (名称, 标语, 描述, 关键词)', | ||
| 659 | + `design_preferences` JSON DEFAULT NULL COMMENT '设计偏好 (风格, 主色调)', | ||
| 660 | + `sitemap_structure` JSON DEFAULT NULL COMMENT '站点地图结构定义 (JSON, 描述栏目层级和类型)', | ||
| 661 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', | ||
| 662 | + PRIMARY KEY (`id`), | ||
| 663 | + UNIQUE KEY `uk_website_config_project` (`project_id`), | ||
| 664 | + CONSTRAINT `fk_website_config_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`) ON DELETE CASCADE | ||
| 665 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI网站构建器配置(存储站点地图)'; | ||
| 666 | + | ||
| 667 | +-- 网站栏目结构表 (支持无限层级) | ||
| 668 | +-- 定义网站的栏目结构。 | ||
| 669 | +CREATE TABLE `ai_website_channels` ( | ||
| 670 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '网站栏目主键ID', | ||
| 671 | + `project_id` INT NOT NULL COMMENT '所属网站项目ID (外键)', | ||
| 672 | + `parent_id` INT DEFAULT NULL COMMENT '父栏目ID (用于实现层级结构)', | ||
| 673 | + `name` VARCHAR(100) NOT NULL COMMENT '栏目名称 (如: 公司介绍, 新闻中心)', | ||
| 674 | + `path` VARCHAR(100) NOT NULL COMMENT 'URL路径 (如: /about, /news)', | ||
| 675 | + `channel_type` ENUM('single_page', 'article_list', 'product_list', 'custom') NOT NULL COMMENT '栏目类型', | ||
| 676 | + `display_order` INT DEFAULT 0 COMMENT '显示顺序', | ||
| 677 | + `is_visible_in_nav` TINYINT(1) DEFAULT 1 COMMENT '是否在主导航中可见', | ||
| 678 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 679 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 680 | + PRIMARY KEY (`id`), | ||
| 681 | + KEY `idx_channel_project` (`project_id`), | ||
| 682 | + KEY `idx_channel_parent` (`parent_id`), | ||
| 683 | + CONSTRAINT `fk_channel_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`), | ||
| 684 | + CONSTRAINT `fk_channel_parent` FOREIGN KEY (`parent_id`) REFERENCES `ai_website_channels` (`id`) | ||
| 685 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='网站栏目结构表 (支持无限层级)'; | ||
| 686 | + | ||
| 687 | +-- 网站文章内容表 | ||
| 688 | +-- 存储网站栏目下的文章内容。 | ||
| 689 | +CREATE TABLE `ai_website_articles` ( | ||
| 690 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章主键ID', | ||
| 691 | + `project_id` INT NOT NULL COMMENT '所属网站项目ID (外键)', | ||
| 692 | + `channel_id` INT NOT NULL COMMENT '所属文章栏目ID (外键)', | ||
| 693 | + `title` VARCHAR(255) NOT NULL COMMENT '文章标题', | ||
| 694 | + `summary` TEXT DEFAULT NULL COMMENT '文章摘要', | ||
| 695 | + `content` LONGTEXT COMMENT '文章主体内容 (Markdown或HTML)', | ||
| 696 | + `status` ENUM('draft', 'published') DEFAULT 'draft' COMMENT '发布状态', | ||
| 697 | + `published_at` TIMESTAMP NULL DEFAULT NULL COMMENT '发布时间', | ||
| 698 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 699 | + PRIMARY KEY (`id`), | ||
| 700 | + KEY `idx_article_project_channel` (`project_id`, `channel_id`), | ||
| 701 | + CONSTRAINT `fk_website_article_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`), | ||
| 702 | + CONSTRAINT `fk_website_article_channel` FOREIGN KEY (`channel_id`) REFERENCES `ai_website_channels` (`id`) | ||
| 703 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='网站文章内容表'; | ||
| 704 | + | ||
| 705 | +-- 网站产品内容表 | ||
| 706 | +-- 存储网站栏目下的产品内容。 | ||
| 707 | +CREATE TABLE `ai_website_products` ( | ||
| 708 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '产品主键ID', | ||
| 709 | + `project_id` INT NOT NULL COMMENT '所属网站项目ID (外键)', | ||
| 710 | + `channel_id` INT NOT NULL COMMENT '所属产品栏目ID (外键)', | ||
| 711 | + `name` VARCHAR(255) NOT NULL COMMENT '产品名称', | ||
| 712 | + `description` LONGTEXT COMMENT '产品详细描述', | ||
| 713 | + `specifications` JSON DEFAULT NULL COMMENT '产品规格 (JSON格式)', | ||
| 714 | + `main_image_url` VARCHAR(255) DEFAULT NULL COMMENT '产品主图URL', | ||
| 715 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 716 | + PRIMARY KEY (`id`), | ||
| 717 | + KEY `idx_product_project_channel` (`project_id`, `channel_id`), | ||
| 718 | + CONSTRAINT `fk_website_product_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`), | ||
| 719 | + CONSTRAINT `fk_website_product_channel` FOREIGN KEY (`channel_id`) REFERENCES `ai_website_channels` (`id`) | ||
| 720 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='网站产品内容表'; | ||
| 721 | + | ||
| 722 | +-- 网站AI生成任务表(支持父子任务) | ||
| 723 | +-- 记录网站生成的请求和状态。 | ||
| 724 | +CREATE TABLE `ai_website_generation_tasks` ( | ||
| 725 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务主键ID', | ||
| 726 | + `project_id` INT NOT NULL COMMENT '网站项目ID (外键)', | ||
| 727 | + `user_id` INT NOT NULL COMMENT '发起用户ID (外键)', | ||
| 728 | + `parent_task_id` INT DEFAULT NULL COMMENT '父任务ID (用于父子任务)', | ||
| 729 | + `task_type` ENUM('full_site', 'generate_shell', 'generate_page_content', 'regenerate_channel') NOT NULL COMMENT '任务类型', | ||
| 730 | + `target_id` INT DEFAULT NULL COMMENT '任务目标ID (如特定页面或栏目ID)', | ||
| 731 | + `status` ENUM('pending','processing','completed','failed', 'waiting_for_children') DEFAULT 'pending' COMMENT '任务状态', | ||
| 732 | + `dify_api_config_id` INT DEFAULT NULL COMMENT '调用的AI配置ID (外键)', | ||
| 733 | + `config_snapshot` JSON DEFAULT NULL COMMENT '生成时刻的配置快照', | ||
| 734 | + `error_message` TEXT DEFAULT NULL COMMENT '错误信息', | ||
| 735 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 736 | + `completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间', | ||
| 737 | + PRIMARY KEY (`id`), | ||
| 738 | + KEY `idx_website_task_project` (`project_id`), | ||
| 739 | + KEY `idx_website_task_parent` (`parent_task_id`), | ||
| 740 | + CONSTRAINT `fk_website_task_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`), | ||
| 741 | + CONSTRAINT `fk_website_task_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`), | ||
| 742 | + CONSTRAINT `fk_website_task_parent` FOREIGN KEY (`parent_task_id`) REFERENCES `ai_website_generation_tasks` (`id`), | ||
| 743 | + CONSTRAINT `fk_website_task_ai_config` FOREIGN KEY (`dify_api_config_id`) REFERENCES `ai_dify_api_configs` (`id`) | ||
| 744 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='网站AI生成任务表(支持父子任务)'; | ||
| 745 | + | ||
| 746 | +-- 生成的网站页面(映射到栏目或内容项) | ||
| 747 | +-- 存储AI生成的最终网站页面HTML代码。 | ||
| 748 | +CREATE TABLE `ai_generated_website_pages` ( | ||
| 749 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '生成页面主键ID', | ||
| 750 | + `project_id` INT NOT NULL COMMENT '来源网站项目ID (外键)', | ||
| 751 | + `task_id` INT NOT NULL COMMENT '来源生成任务ID (外键)', | ||
| 752 | + `pageable_type` VARCHAR(100) NOT NULL COMMENT '关联类型 (Channel, Article, Product)', | ||
| 753 | + `pageable_id` INT NOT NULL COMMENT '关联类型ID', | ||
| 754 | + `file_name` VARCHAR(100) NOT NULL COMMENT '文件名 (如: index.html, news/article-1.html)', | ||
| 755 | + `html_content` LONGTEXT COMMENT '页面的完整HTML内容', | ||
| 756 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间', | ||
| 757 | + PRIMARY KEY (`id`), | ||
| 758 | + KEY `idx_pageable` (`pageable_type`, `pageable_id`), | ||
| 759 | + CONSTRAINT `fk_website_page_project` FOREIGN KEY (`project_id`) REFERENCES `ai_website_projects` (`id`), | ||
| 760 | + CONSTRAINT `fk_website_page_task` FOREIGN KEY (`task_id`) REFERENCES `ai_website_generation_tasks` (`id`) | ||
| 761 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='生成的网站页面(映射到栏目或内容项)'; | ||
| 762 | + | ||
| 763 | +-- ==================================================================================================== | ||
| 764 | +-- 8) 内容发布系统 | ||
| 765 | +-- ==================================================================================================== | ||
| 766 | + | ||
| 767 | +-- 目标网站类型表 | ||
| 768 | +-- 对目标平台进行大类划分。 | ||
| 769 | +CREATE TABLE `ai_publishing_platform_types` ( | ||
| 770 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '平台类型主键ID', | ||
| 771 | + `name` VARCHAR(50) NOT NULL COMMENT '平台类型名称(如 Blog, Social, Video)', | ||
| 772 | + `description` VARCHAR(255) DEFAULT NULL COMMENT '类型描述', | ||
| 773 | + `icon` VARCHAR(100) DEFAULT NULL COMMENT '图标标识', | ||
| 774 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 775 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 776 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 777 | + PRIMARY KEY (`id`), | ||
| 778 | + UNIQUE KEY `uk_platform_types_name` (`name`) | ||
| 779 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='目标网站类型表'; | ||
| 780 | + | ||
| 781 | +-- 目标网站表 | ||
| 782 | +-- 定义支持发布的目标平台及其API配置模板。 | ||
| 783 | +CREATE TABLE `ai_publishing_platforms` ( | ||
| 784 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '目标网站主键ID', | ||
| 785 | + `type_id` INT NOT NULL COMMENT '目标网站类型ID(外键)', | ||
| 786 | + `name` VARCHAR(100) NOT NULL COMMENT '目标网站名称(如 WordPress, LinkedIn, 小红书)', | ||
| 787 | + `code` VARCHAR(50) NOT NULL COMMENT '目标网站代码(唯一)', | ||
| 788 | + `description` VARCHAR(255) DEFAULT NULL COMMENT '目标网站说明', | ||
| 789 | + `icon` VARCHAR(100) DEFAULT NULL COMMENT '目标网站图标', | ||
| 790 | + `auth_type` ENUM('oauth','api_key','basic','custom') NOT NULL COMMENT '认证类型', | ||
| 791 | + `api_config_template` JSON DEFAULT NULL COMMENT 'API 配置模板(JSON schema)', | ||
| 792 | + `character_limit` INT DEFAULT NULL COMMENT '目标网站字符限制(如微博)', | ||
| 793 | + `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用', | ||
| 794 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 795 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 796 | + PRIMARY KEY (`id`), | ||
| 797 | + UNIQUE KEY `uk_platforms_code` (`code`), | ||
| 798 | + KEY `idx_platforms_type` (`type_id`), | ||
| 799 | + CONSTRAINT `fk_platform_type` FOREIGN KEY (`type_id`) REFERENCES `ai_publishing_platform_types` (`id`) | ||
| 800 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='目标网站表'; | ||
| 801 | + | ||
| 802 | +-- 企业在目标网站的配置(API key/Token等) | ||
| 803 | +-- 存储公司对特定平台的授权信息。 | ||
| 804 | +CREATE TABLE `ai_company_platform_configs` ( | ||
| 805 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '企业平台配置主键ID', | ||
| 806 | + `company_id` INT NOT NULL COMMENT '授权公司ID(外键)', | ||
| 807 | + `platform_id` INT NOT NULL COMMENT '目标网站ID(外键)', | ||
| 808 | + `config_data` JSON NOT NULL COMMENT '目标网站API配置信息(如 token、client_id)', | ||
| 809 | + `account_name` VARCHAR(100) DEFAULT NULL COMMENT '企业在该目标网站的账号名/展示名', | ||
| 810 | + `is_enabled` TINYINT(1) DEFAULT 1 COMMENT '是否启用该配置', | ||
| 811 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 812 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 813 | + PRIMARY KEY (`id`), | ||
| 814 | + UNIQUE KEY `uk_company_platform` (`company_id`,`platform_id`), | ||
| 815 | + KEY `idx_company_platform_company` (`company_id`), | ||
| 816 | + KEY `idx_company_platform_platform` (`platform_id`), | ||
| 817 | + CONSTRAINT `fk_company_platform_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) ON DELETE CASCADE, | ||
| 818 | + CONSTRAINT `fk_company_platform_platform` FOREIGN KEY (`platform_id`) REFERENCES `ai_publishing_platforms` (`id`) | ||
| 819 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='企业在目标网站的配置(API key/Token等)'; | ||
| 820 | + | ||
| 821 | +-- 定时发布任务表 | ||
| 822 | +-- 管理计划在未来某个时间点执行的发布任务。 | ||
| 823 | +CREATE TABLE `ai_scheduled_publishing_tasks` ( | ||
| 824 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '定时发布任务主键ID', | ||
| 825 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 826 | + `publishable_type` VARCHAR(50) DEFAULT 'article' COMMENT '发布内容类型(article, landing_page)', | ||
| 827 | + `publishable_id` INT NOT NULL COMMENT '发布内容ID', | ||
| 828 | + `article_id` INT DEFAULT NULL COMMENT '要发布的文章ID(外键) - 兼容旧版', | ||
| 829 | + `platform_config_id` INT NOT NULL COMMENT '使用的企业平台配置ID(外键)', | ||
| 830 | + `schedule_time` TIMESTAMP NOT NULL COMMENT '计划发布时间', | ||
| 831 | + `status` ENUM('scheduled','processing','success','failed','cancelled') DEFAULT 'scheduled' COMMENT '任务状态', | ||
| 832 | + `retry_count` INT DEFAULT 0 COMMENT '已重试次数', | ||
| 833 | + `max_retries` INT DEFAULT 3 COMMENT '最大重试次数', | ||
| 834 | + `last_attempt_at` TIMESTAMP NULL DEFAULT NULL COMMENT '最近一次尝试执行时间', | ||
| 835 | + `error_message` TEXT DEFAULT NULL COMMENT '错误信息(失败时记录)', | ||
| 836 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 837 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 838 | + PRIMARY KEY (`id`), | ||
| 839 | + KEY `idx_schedule_company_time` (`company_id`, `schedule_time`), | ||
| 840 | + KEY `idx_schedule_status` (`status`), | ||
| 841 | + CONSTRAINT `fk_schedule_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 842 | + CONSTRAINT `fk_schedule_platform_config` FOREIGN KEY (`platform_config_id`) REFERENCES `ai_company_platform_configs` (`id`) | ||
| 843 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时发布任务表'; | ||
| 844 | + | ||
| 845 | +-- 目标网站发布记录表(执行日志) | ||
| 846 | +-- 记录所有发布操作的执行结果。 | ||
| 847 | +CREATE TABLE `ai_publishing_records` ( | ||
| 848 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '发布记录主键ID', | ||
| 849 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 850 | + `publishable_type` VARCHAR(50) DEFAULT 'article' COMMENT '发布内容类型(article, landing_page)', | ||
| 851 | + `publishable_id` INT NOT NULL COMMENT '发布内容ID', | ||
| 852 | + `article_id` INT DEFAULT NULL COMMENT '文章ID(外键) - 兼容旧版', | ||
| 853 | + `platform_config_id` INT NOT NULL COMMENT '目标网站配置ID(外键)', | ||
| 854 | + `scheduled_task_id` INT DEFAULT NULL COMMENT '来源的定时任务ID(如果是定时发布则关联)', | ||
| 855 | + `status` ENUM('scheduled','published','failed') DEFAULT 'scheduled' COMMENT '发布记录状态', | ||
| 856 | + `publish_url` VARCHAR(500) DEFAULT NULL COMMENT '发布后目标网站返回的文章URL', | ||
| 857 | + `external_post_id` VARCHAR(255) DEFAULT NULL COMMENT '目标网站返回的外部帖文/文章ID', | ||
| 858 | + `publish_time` TIMESTAMP NULL DEFAULT NULL COMMENT '实际发布时间', | ||
| 859 | + `error_message` TEXT DEFAULT NULL COMMENT '失败原因(若失败)', | ||
| 860 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', | ||
| 861 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', | ||
| 862 | + PRIMARY KEY (`id`), | ||
| 863 | + KEY `idx_publishing_company_time` (`company_id`,`publish_time`), | ||
| 864 | + KEY `idx_publishing_status` (`status`), | ||
| 865 | + CONSTRAINT `fk_publishing_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 866 | + CONSTRAINT `fk_publishing_platform_config` FOREIGN KEY (`platform_config_id`) REFERENCES `ai_company_platform_configs` (`id`), | ||
| 867 | + CONSTRAINT `fk_publishing_scheduled_task` FOREIGN KEY (`scheduled_task_id`) REFERENCES `ai_scheduled_publishing_tasks` (`id`) | ||
| 868 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='目标网站发布记录表(执行日志)'; | ||
| 869 | + | ||
| 870 | +-- ==================================================================================================== | ||
| 871 | +-- 9) 关键词与话题管理 (SEO/内容规划) | ||
| 872 | +-- ==================================================================================================== | ||
| 873 | + | ||
| 874 | +-- 关键词表 | ||
| 875 | +-- 管理用于内容生成和SEO的关键词。 | ||
| 876 | +CREATE TABLE `ai_keywords` ( | ||
| 877 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '关键词主键ID', | ||
| 878 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 879 | + `keyword` VARCHAR(255) NOT NULL COMMENT '关键词文本', | ||
| 880 | + `search_volume` INT DEFAULT 0 COMMENT '搜索量估算', | ||
| 881 | + `cpc` DECIMAL(10,2) DEFAULT 0.00 COMMENT '估计每次点击成本', | ||
| 882 | + `difficulty` TINYINT DEFAULT 0 COMMENT '关键词难度评分(0-100)', | ||
| 883 | + `source` ENUM('baidu','google','manual','expansion','import') DEFAULT 'manual' COMMENT '来源类型', | ||
| 884 | + `status` ENUM('pending','planned','generated','published') DEFAULT 'pending' COMMENT '关键词状态', | ||
| 885 | + `tags` VARCHAR(255) DEFAULT NULL COMMENT '关键词标签,逗号分隔', | ||
| 886 | + `usage_count` INT DEFAULT 0 COMMENT '关键词被文章使用次数', | ||
| 887 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 888 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 889 | + PRIMARY KEY (`id`), | ||
| 890 | + UNIQUE KEY `uk_company_keyword` (`company_id`,`keyword`), | ||
| 891 | + KEY `idx_keywords_company_status` (`company_id`,`status`), | ||
| 892 | + CONSTRAINT `fk_keyword_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) | ||
| 893 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='关键词表'; | ||
| 894 | + | ||
| 895 | +-- 关键词扩展表(保存 related_searches) | ||
| 896 | +-- 存储关键词的扩展词(如相关搜索词)。 | ||
| 897 | +CREATE TABLE `ai_keyword_expansions` ( | ||
| 898 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '关键词扩展主键ID', | ||
| 899 | + `keyword_id` INT NOT NULL COMMENT '原始关键词ID(外键)', | ||
| 900 | + `related_keyword` VARCHAR(255) NOT NULL COMMENT '扩展关键词(来自 related_searches 等)', | ||
| 901 | + `source` ENUM('baidu','google','import') DEFAULT 'google' COMMENT '扩展来源', | ||
| 902 | + `raw_response` JSON DEFAULT NULL COMMENT 'API 原始返回(便于溯源)', | ||
| 903 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 904 | + PRIMARY KEY (`id`), | ||
| 905 | + KEY `idx_keyword_exp_keyword` (`keyword_id`), | ||
| 906 | + CONSTRAINT `fk_keyword_expansion_keyword` FOREIGN KEY (`keyword_id`) REFERENCES `ai_keywords` (`id`) ON DELETE CASCADE | ||
| 907 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='关键词扩展表(保存 related_searches)'; | ||
| 908 | + | ||
| 909 | +-- 话题表 | ||
| 910 | +-- 管理更宽泛的内容主题。 | ||
| 911 | +CREATE TABLE `ai_topics` ( | ||
| 912 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '话题主键ID', | ||
| 913 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 914 | + `source_task_id` INT DEFAULT NULL COMMENT '来源的抓取任务ID(外键,可空)', | ||
| 915 | + `title` VARCHAR(255) NOT NULL COMMENT '话题标题', | ||
| 916 | + `description` TEXT DEFAULT NULL COMMENT '话题描述或摘要', | ||
| 917 | + `source_url` VARCHAR(500) DEFAULT NULL COMMENT '原始来源链接(可选)', | ||
| 918 | + `status` ENUM('raw','curated','rejected') DEFAULT 'raw' COMMENT '话题状态', | ||
| 919 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 920 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 921 | + PRIMARY KEY (`id`), | ||
| 922 | + KEY `idx_topics_company_status` (`company_id`,`status`), | ||
| 923 | + KEY `idx_topics_source_task` (`source_task_id`), | ||
| 924 | + CONSTRAINT `fk_topic_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 925 | + CONSTRAINT `fk_topic_source_task` FOREIGN KEY (`source_task_id`) REFERENCES `ai_search_sources_tasks` (`id`) | ||
| 926 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='话题表'; | ||
| 927 | + | ||
| 928 | +-- 话题与关键词的多对多映射 | ||
| 929 | +-- 建立话题和关键词之间的关联。 | ||
| 930 | +CREATE TABLE `ai_topic_keywords` ( | ||
| 931 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '话题-关键词关联主键ID', | ||
| 932 | + `topic_id` INT NOT NULL COMMENT '话题ID(外键)', | ||
| 933 | + `keyword_id` INT NOT NULL COMMENT '关键词ID(外键)', | ||
| 934 | + PRIMARY KEY (`id`), | ||
| 935 | + UNIQUE KEY `uk_topic_keyword` (`topic_id`,`keyword_id`), | ||
| 936 | + KEY `idx_topic_keywords_topic` (`topic_id`), | ||
| 937 | + KEY `idx_topic_keywords_keyword` (`keyword_id`), | ||
| 938 | + CONSTRAINT `fk_topic_keyword_topic` FOREIGN KEY (`topic_id`) REFERENCES `ai_topics` (`id`) ON DELETE CASCADE, | ||
| 939 | + CONSTRAINT `fk_topic_keyword_keyword` FOREIGN KEY (`keyword_id`) REFERENCES `ai_keywords` (`id`) ON DELETE CASCADE | ||
| 940 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='话题与关键词的多对多映射'; | ||
| 941 | + | ||
| 942 | +-- AI数据源配置表 | ||
| 943 | +-- 配置用于抓取话题的外部数据源(如SERP API)。 | ||
| 944 | +CREATE TABLE `ai_search_sources_api` ( | ||
| 945 | + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据源配置主键ID', | ||
| 946 | + `name` VARCHAR(100) NOT NULL COMMENT '数据来源名称 (如 百度前10、Google also ask)', | ||
| 947 | + `source_type` ENUM('serp_api', 'rss', 'custom_api', 'web_scraping', 'database') NOT NULL COMMENT '来源类型', | ||
| 948 | + `api_config` JSON NOT NULL COMMENT 'API 配置', | ||
| 949 | + `result_parser` JSON NOT NULL COMMENT '结果解析规则', | ||
| 950 | + `is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用', | ||
| 951 | + `priority` TINYINT UNSIGNED DEFAULT 10 COMMENT '优先级 (1-100, 数字越小优先级越高)', | ||
| 952 | + `rate_limit` JSON COMMENT '限流配置 {"requests": 100, "period": 3600}', | ||
| 953 | + `health_check` JSON COMMENT '健康检查配置', | ||
| 954 | + `metadata` JSON COMMENT '元数据 (描述、版本等)', | ||
| 955 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 956 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||
| 957 | + PRIMARY KEY (`id`), | ||
| 958 | + INDEX `idx_source_name` (`name`), | ||
| 959 | + INDEX `idx_source_type` (`source_type`), | ||
| 960 | + INDEX `idx_source_active` (`is_active`), | ||
| 961 | + INDEX `idx_source_priority` (`priority`) | ||
| 962 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI数据源配置表'; | ||
| 963 | + | ||
| 964 | +-- AI数据源抓取任务表 | ||
| 965 | +-- 记录从外部数据源抓取话题的任务。 | ||
| 966 | +CREATE TABLE `ai_search_sources_tasks` ( | ||
| 967 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '话题抓取任务主键ID', | ||
| 968 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 969 | + `user_id` INT NOT NULL COMMENT '发起任务的用户ID(外键)', | ||
| 970 | + `topic_source_id` INT NOT NULL COMMENT '话题来源配置ID(外键)', | ||
| 971 | + `keywords` TEXT DEFAULT NULL COMMENT '用于本次抓取的关键词快照(逗号分隔)', | ||
| 972 | + `topic_id` INT DEFAULT NULL COMMENT '关联的话题ID(外键,可空)', | ||
| 973 | + `keyword_id` INT DEFAULT NULL COMMENT '关联的关键词ID(外键,可空)', | ||
| 974 | + `status` ENUM('pending','processing','completed','failed') DEFAULT 'pending' COMMENT '任务状态', | ||
| 975 | + `result_count` INT DEFAULT 0 COMMENT '抓取结果数量', | ||
| 976 | + `raw_response` JSON DEFAULT NULL COMMENT 'API 原始返回(JSON)', | ||
| 977 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 978 | + `completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '任务完成时间', | ||
| 979 | + PRIMARY KEY (`id`), | ||
| 980 | + KEY `idx_topic_fetch_company` (`company_id`), | ||
| 981 | + KEY `idx_topic_fetch_source` (`topic_source_id`), | ||
| 982 | + KEY `idx_topic_fetch_status` (`status`), | ||
| 983 | + CONSTRAINT `fk_search_task_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 984 | + CONSTRAINT `fk_search_task_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`), | ||
| 985 | + CONSTRAINT `fk_search_task_source` FOREIGN KEY (`topic_source_id`) REFERENCES `ai_search_sources_api` (`id`) | ||
| 986 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI数据源抓取任务表'; | ||
| 987 | + | ||
| 988 | +-- ==================================================================================================== | ||
| 989 | +-- 10) 任务关联表 (连接生成任务与资源) | ||
| 990 | +-- ==================================================================================================== | ||
| 991 | + | ||
| 992 | +-- 任务参考链接明细(可标注高度参考) | ||
| 993 | +-- 存储文章生成任务中使用的参考链接。 | ||
| 994 | +CREATE TABLE `ai_task_references` ( | ||
| 995 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务参考链接主键ID', | ||
| 996 | + `task_id` INT NOT NULL COMMENT '对应任务ID(外键)', | ||
| 997 | + `url` VARCHAR(500) NOT NULL COMMENT '参考链接URL', | ||
| 998 | + `title` VARCHAR(255) DEFAULT NULL COMMENT '页面标题(可选)', | ||
| 999 | + `source_engine` VARCHAR(50) DEFAULT NULL COMMENT '来源引擎(google/baidu/zhihu等)', | ||
| 1000 | + `position` INT DEFAULT NULL COMMENT '在搜索结果中的排名位置', | ||
| 1001 | + `is_high_reference` TINYINT(1) DEFAULT 0 COMMENT '是否标记为高度参考(1是)', | ||
| 1002 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 1003 | + PRIMARY KEY (`id`), | ||
| 1004 | + KEY `idx_task_references_task` (`task_id`), | ||
| 1005 | + CONSTRAINT `fk_task_reference_task` FOREIGN KEY (`task_id`) REFERENCES `ai_article_generation_tasks` (`id`) ON DELETE CASCADE | ||
| 1006 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='任务参考链接明细(可标注高度参考)'; | ||
| 1007 | + | ||
| 1008 | +-- 生成任务与关键词映射(规划用) | ||
| 1009 | +-- 将生成任务与计划使用的关键词关联。 | ||
| 1010 | +CREATE TABLE `ai_task_keywords` ( | ||
| 1011 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务-关键词关联主键ID', | ||
| 1012 | + `task_id` INT NOT NULL COMMENT '生成任务ID(外键)', | ||
| 1013 | + `keyword_id` INT NOT NULL COMMENT '关键词ID(外键)', | ||
| 1014 | + `is_primary` TINYINT(1) DEFAULT 0 COMMENT '是否主关键词', | ||
| 1015 | + PRIMARY KEY (`id`), | ||
| 1016 | + UNIQUE KEY `uk_task_keyword` (`task_id`,`keyword_id`), | ||
| 1017 | + KEY `idx_task_keywords_task` (`task_id`), | ||
| 1018 | + KEY `idx_task_keywords_keyword` (`keyword_id`), | ||
| 1019 | + CONSTRAINT `fk_task_keyword_task` FOREIGN KEY (`task_id`) REFERENCES `ai_article_generation_tasks` (`id`) ON DELETE CASCADE, | ||
| 1020 | + CONSTRAINT `fk_task_keyword_keyword` FOREIGN KEY (`keyword_id`) REFERENCES `ai_keywords` (`id`) ON DELETE CASCADE | ||
| 1021 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='生成任务与关键词映射(规划用)'; | ||
| 1022 | + | ||
| 1023 | +-- 生成任务与话题映射 | ||
| 1024 | +-- 将生成任务与相关话题关联。 | ||
| 1025 | +CREATE TABLE `ai_task_topics` ( | ||
| 1026 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务-话题关联主键ID', | ||
| 1027 | + `task_id` INT NOT NULL COMMENT '生成任务ID(外键)', | ||
| 1028 | + `topic_id` INT NOT NULL COMMENT '话题ID(外键)', | ||
| 1029 | + PRIMARY KEY (`id`), | ||
| 1030 | + UNIQUE KEY `uk_task_topic` (`task_id`,`topic_id`), | ||
| 1031 | + KEY `idx_task_topics_task` (`task_id`), | ||
| 1032 | + KEY `idx_task_topics_topic` (`topic_id`), | ||
| 1033 | + CONSTRAINT `fk_task_topic_task` FOREIGN KEY (`task_id`) REFERENCES `ai_article_generation_tasks` (`id`) ON DELETE CASCADE, | ||
| 1034 | + CONSTRAINT `fk_task_topic_topic` FOREIGN KEY (`topic_id`) REFERENCES `ai_topics` (`id`) ON DELETE CASCADE | ||
| 1035 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='生成任务与话题映射'; | ||
| 1036 | + | ||
| 1037 | +-- 任务知识库来源(公司库或上传的解析内容) | ||
| 1038 | +-- 指定生成任务使用的知识来源。 | ||
| 1039 | +CREATE TABLE `ai_task_knowledge_sources` ( | ||
| 1040 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '任务知识来源主键ID', | ||
| 1041 | + `task_id` INT NOT NULL COMMENT '任务ID(外键)', | ||
| 1042 | + `knowledge_type` ENUM('company','uploaded') NOT NULL COMMENT '知识来源类型(公司知识库/上传文件)', | ||
| 1043 | + `knowledge_id` INT DEFAULT NULL COMMENT '当 knowledge_type=uploaded 时,关联 ai_uploaded_files.id', | ||
| 1044 | + `content` LONGTEXT DEFAULT NULL COMMENT '若上传文件被解析,存解析后的文本内容', | ||
| 1045 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 1046 | + PRIMARY KEY (`id`), | ||
| 1047 | + KEY `idx_task_knowledge_task` (`task_id`), | ||
| 1048 | + KEY `idx_task_knowledge_knowledge` (`knowledge_id`), | ||
| 1049 | + CONSTRAINT `fk_task_knowledge_task` FOREIGN KEY (`task_id`) REFERENCES `ai_article_generation_tasks` (`id`) ON DELETE CASCADE, | ||
| 1050 | + CONSTRAINT `fk_task_knowledge_file` FOREIGN KEY (`knowledge_id`) REFERENCES `ai_uploaded_files` (`id`) | ||
| 1051 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='任务知识库来源(公司库或上传的解析内容)'; | ||
| 1052 | + | ||
| 1053 | +-- 文章与关键词关联(实际使用) | ||
| 1054 | +-- 将最终生成的文章与实际使用的关键词关联。 | ||
| 1055 | +CREATE TABLE `ai_article_keywords` ( | ||
| 1056 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章关键词关联主键ID', | ||
| 1057 | + `article_id` INT NOT NULL COMMENT '文章ID(外键)', | ||
| 1058 | + `keyword_id` INT NOT NULL COMMENT '关键词ID(外键)', | ||
| 1059 | + `is_primary` TINYINT(1) DEFAULT 0 COMMENT '是否主关键词(1主/0辅)', | ||
| 1060 | + PRIMARY KEY (`id`), | ||
| 1061 | + UNIQUE KEY `uk_article_keyword` (`article_id`,`keyword_id`), | ||
| 1062 | + KEY `idx_article_keywords_article` (`article_id`), | ||
| 1063 | + KEY `idx_article_keywords_keyword` (`keyword_id`), | ||
| 1064 | + CONSTRAINT `fk_article_keyword_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE, | ||
| 1065 | + CONSTRAINT `fk_article_keyword_keyword` FOREIGN KEY (`keyword_id`) REFERENCES `ai_keywords` (`id`) ON DELETE CASCADE | ||
| 1066 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章与关键词关联(实际使用)'; | ||
| 1067 | + | ||
| 1068 | +-- 文章与话题关联 | ||
| 1069 | +-- 将最终生成的文章与相关话题关联。 | ||
| 1070 | +CREATE TABLE `ai_article_topics` ( | ||
| 1071 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '文章话题关联主键ID', | ||
| 1072 | + `article_id` INT NOT NULL COMMENT '文章ID(外键)', | ||
| 1073 | + `topic_id` INT NOT NULL COMMENT '话题ID(外键)', | ||
| 1074 | + PRIMARY KEY (`id`), | ||
| 1075 | + UNIQUE KEY `uk_article_topic` (`article_id`,`topic_id`), | ||
| 1076 | + KEY `idx_article_topics_article` (`article_id`), | ||
| 1077 | + KEY `idx_article_topics_topic` (`topic_id`), | ||
| 1078 | + CONSTRAINT `fk_article_topic_article` FOREIGN KEY (`article_id`) REFERENCES `ai_generated_articles` (`id`) ON DELETE CASCADE, | ||
| 1079 | + CONSTRAINT `fk_article_topic_topic` FOREIGN KEY (`topic_id`) REFERENCES `ai_topics` (`id`) ON DELETE CASCADE | ||
| 1080 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文章与话题关联'; | ||
| 1081 | + | ||
| 1082 | +-- ==================================================================================================== | ||
| 1083 | +-- 11) 统计与活动日志 | ||
| 1084 | +-- ==================================================================================================== | ||
| 1085 | + | ||
| 1086 | +-- 用户活动日志(审计/分析) | ||
| 1087 | +-- 记录用户的常规活动,用于行为分析。 | ||
| 1088 | +CREATE TABLE `ai_user_activity_logs` ( | ||
| 1089 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '用户活动日志主键ID', | ||
| 1090 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 1091 | + `user_id` INT NOT NULL COMMENT '用户ID(外键)', | ||
| 1092 | + `activity_type` VARCHAR(50) NOT NULL COMMENT '活动类型(如 login/create_article)', | ||
| 1093 | + `activity_details` JSON DEFAULT NULL COMMENT '活动详情(JSON)', | ||
| 1094 | + `ip_address` VARCHAR(45) DEFAULT NULL COMMENT '操作IP', | ||
| 1095 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 1096 | + PRIMARY KEY (`id`), | ||
| 1097 | + KEY `idx_activity_company_user` (`company_id`,`user_id`), | ||
| 1098 | + KEY `idx_activity_type` (`activity_type`), | ||
| 1099 | + CONSTRAINT `fk_activity_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`), | ||
| 1100 | + CONSTRAINT `fk_activity_user` FOREIGN KEY (`user_id`) REFERENCES `ai_users` (`id`) | ||
| 1101 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户活动日志(审计/分析)'; | ||
| 1102 | + | ||
| 1103 | +-- 公司使用统计 | ||
| 1104 | +-- 统计公司的资源使用情况,用于计费和容量管理。 | ||
| 1105 | +CREATE TABLE `ai_company_usage_stats` ( | ||
| 1106 | + `id` INT NOT NULL AUTO_INCREMENT COMMENT '公司用量统计主键ID', | ||
| 1107 | + `company_id` INT NOT NULL COMMENT '公司ID(外键)', | ||
| 1108 | + `stat_date` DATE NOT NULL COMMENT '统计日期', | ||
| 1109 | + `article_count` INT DEFAULT 0 COMMENT '当日生成文章数量', | ||
| 1110 | + `ai_token_usage` BIGINT DEFAULT 0 COMMENT '当日 AI 令牌使用量', | ||
| 1111 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', | ||
| 1112 | + PRIMARY KEY (`id`), | ||
| 1113 | + UNIQUE KEY `uk_company_date` (`company_id`,`stat_date`), | ||
| 1114 | + KEY `idx_usage_company` (`company_id`), | ||
| 1115 | + CONSTRAINT `fk_usage_stats_company` FOREIGN KEY (`company_id`) REFERENCES `ai_companies` (`id`) | ||
| 1116 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='公司使用统计'; | ||
| 1117 | + | ||
| 1118 | +SET FOREIGN_KEY_CHECKS = 1; |
| 1 | +package com.aigeo.ai.controller; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.Feature; | ||
| 4 | +import com.aigeo.ai.service.FeatureService; | ||
| 5 | +import com.aigeo.common.Result; | ||
| 6 | +import com.aigeo.common.ResultPage; | ||
| 7 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 8 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 11 | +import org.springframework.data.domain.Page; | ||
| 12 | +import org.springframework.data.domain.PageRequest; | ||
| 13 | +import org.springframework.data.domain.Pageable; | ||
| 14 | +import org.springframework.web.bind.annotation.*; | ||
| 15 | + | ||
| 16 | +import java.util.List; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * AI功能模块控制器 | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +@RestController | ||
| 23 | +@RequestMapping("/api/features") | ||
| 24 | +@Tag(name = "AI功能管理", description = "AI功能模块相关接口") | ||
| 25 | +public class FeatureController { | ||
| 26 | + | ||
| 27 | + @Autowired | ||
| 28 | + private FeatureService featureService; | ||
| 29 | + | ||
| 30 | + @PostMapping | ||
| 31 | + @Operation(summary = "创建AI功能", description = "创建新的AI功能") | ||
| 32 | + public Result<Feature> createFeature(@RequestBody Feature feature) { | ||
| 33 | + try { | ||
| 34 | + Feature savedFeature = featureService.save(feature); | ||
| 35 | + return Result.success("功能创建成功", savedFeature); | ||
| 36 | + } catch (Exception e) { | ||
| 37 | + log.error("创建AI功能失败", e); | ||
| 38 | + return Result.error("功能创建失败"); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @GetMapping("/{id}") | ||
| 43 | + @Operation(summary = "获取AI功能详情", description = "根据ID获取AI功能详情") | ||
| 44 | + public Result<Feature> getFeatureById(@PathVariable Integer id) { | ||
| 45 | + try { | ||
| 46 | + return featureService.findById(id) | ||
| 47 | + .map(feature -> Result.success("查询成功", feature)) | ||
| 48 | + .orElse(Result.error("功能不存在")); | ||
| 49 | + } catch (Exception e) { | ||
| 50 | + log.error("获取AI功能详情失败, id: {}", id, e); | ||
| 51 | + return Result.error("查询失败"); | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + @GetMapping | ||
| 56 | + @Operation(summary = "获取AI功能列表", description = "获取所有AI功能列表") | ||
| 57 | + public Result<List<Feature>> getAllFeatures() { | ||
| 58 | + try { | ||
| 59 | + List<Feature> features = featureService.findAll(); | ||
| 60 | + return Result.success("查询成功", features); | ||
| 61 | + } catch (Exception e) { | ||
| 62 | + log.error("获取AI功能列表失败", e); | ||
| 63 | + return Result.error("查询失败"); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @GetMapping("/active") | ||
| 68 | + @Operation(summary = "获取启用的AI功能列表", description = "获取所有启用的AI功能列表") | ||
| 69 | + public Result<List<Feature>> getActiveFeatures() { | ||
| 70 | + try { | ||
| 71 | + List<Feature> features = featureService.findActiveFeatures(); | ||
| 72 | + return Result.success("查询成功", features); | ||
| 73 | + } catch (Exception e) { | ||
| 74 | + log.error("获取启用的AI功能列表失败", e); | ||
| 75 | + return Result.error("查询失败"); | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + @GetMapping("/category/{category}") | ||
| 80 | + @Operation(summary = "根据分类获取AI功能列表", description = "根据分类获取AI功能列表") | ||
| 81 | + public Result<List<Feature>> getFeaturesByCategory(@PathVariable String category) { | ||
| 82 | + try { | ||
| 83 | + List<Feature> features = featureService.findByCategory(category); | ||
| 84 | + return Result.success("查询成功", features); | ||
| 85 | + } catch (Exception e) { | ||
| 86 | + log.error("根据分类获取AI功能列表失败, category: {}", category, e); | ||
| 87 | + return Result.error("查询失败"); | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + @PutMapping("/{id}") | ||
| 92 | + @Operation(summary = "更新AI功能", description = "更新AI功能信息") | ||
| 93 | + public Result<Feature> updateFeature(@PathVariable Integer id, @RequestBody Feature feature) { | ||
| 94 | + try { | ||
| 95 | + if (!featureService.findById(id).isPresent()) { | ||
| 96 | + return Result.error("功能不存在"); | ||
| 97 | + } | ||
| 98 | + feature.setId(id); | ||
| 99 | + Feature updatedFeature = featureService.save(feature); | ||
| 100 | + return Result.success("功能更新成功", updatedFeature); | ||
| 101 | + } catch (Exception e) { | ||
| 102 | + log.error("更新AI功能失败, id: {}", id, e); | ||
| 103 | + return Result.error("功能更新失败"); | ||
| 104 | + } | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + @DeleteMapping("/{id}") | ||
| 108 | + @Operation(summary = "删除AI功能", description = "删除指定ID的AI功能") | ||
| 109 | + public Result<String> deleteFeature(@PathVariable Integer id) { | ||
| 110 | + try { | ||
| 111 | + if (!featureService.findById(id).isPresent()) { | ||
| 112 | + return Result.error("功能不存在"); | ||
| 113 | + } | ||
| 114 | + featureService.deleteById(id); | ||
| 115 | + return Result.success("功能删除成功"); | ||
| 116 | + } catch (Exception e) { | ||
| 117 | + log.error("删除AI功能失败, id: {}", id, e); | ||
| 118 | + return Result.error("功能删除失败"); | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | +} |
| 1 | +package com.aigeo.ai.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import org.springframework.beans.BeanUtils; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * Dify API配置DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "Dify API配置DTO") | ||
| 13 | +public class DifyApiConfigDTO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "配置ID") | ||
| 16 | + private Integer id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "公司ID") | ||
| 19 | + private Integer companyId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "配置名称") | ||
| 22 | + private String name; | ||
| 23 | + | ||
| 24 | + @Schema(description = "API Key") | ||
| 25 | + private String apiKey; | ||
| 26 | + | ||
| 27 | + @Schema(description = "API地址") | ||
| 28 | + private String apiUrl; | ||
| 29 | + | ||
| 30 | + @Schema(description = "是否启用") | ||
| 31 | + private Boolean isActive; | ||
| 32 | + | ||
| 33 | + @Schema(description = "备注") | ||
| 34 | + private String remark; | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 将DTO转换为实体类 | ||
| 38 | + */ | ||
| 39 | + public DifyApiConfig toEntity() { | ||
| 40 | + DifyApiConfig entity = new DifyApiConfig(); | ||
| 41 | + BeanUtils.copyProperties(this, entity); | ||
| 42 | + return entity; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 将实体类转换为DTO | ||
| 47 | + */ | ||
| 48 | + public static DifyApiConfigDTO fromEntity(DifyApiConfig entity) { | ||
| 49 | + DifyApiConfigDTO dto = new DifyApiConfigDTO(); | ||
| 50 | + BeanUtils.copyProperties(entity, dto); | ||
| 51 | + return dto; | ||
| 52 | + } | ||
| 53 | +} |
| 1 | +package com.aigeo.ai.dto; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * AI功能模块DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "AI功能模块数据传输对象") | ||
| 13 | +public class FeatureDTO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "功能ID") | ||
| 16 | + private Integer id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "功能标识符", example = "ai_article") | ||
| 19 | + private String featureKey; | ||
| 20 | + | ||
| 21 | + @Schema(description = "功能名称", example = "AI文章生成") | ||
| 22 | + private String name; | ||
| 23 | + | ||
| 24 | + @Schema(description = "功能描述", example = "基于关键词和主题自动生成高质量文章") | ||
| 25 | + private String description; | ||
| 26 | + | ||
| 27 | + @Schema(description = "功能分类", example = "content") | ||
| 28 | + private String category; | ||
| 29 | + | ||
| 30 | + @Schema(description = "是否为高级功能") | ||
| 31 | + private Boolean isPremium; | ||
| 32 | + | ||
| 33 | + @Schema(description = "排序权重") | ||
| 34 | + private Integer sortOrder; | ||
| 35 | + | ||
| 36 | + @Schema(description = "是否启用") | ||
| 37 | + private Boolean isActive; | ||
| 38 | + | ||
| 39 | + @Schema(description = "创建时间") | ||
| 40 | + private LocalDateTime createdAt; | ||
| 41 | +} |
| 1 | +package com.aigeo.ai.entity; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.*; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * AI功能模块实体类 | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Entity | ||
| 13 | +@Table(name = "ai_features") | ||
| 14 | +public class Feature { | ||
| 15 | + | ||
| 16 | + @Id | ||
| 17 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| 18 | + private Integer id; | ||
| 19 | + | ||
| 20 | + @Column(name = "feature_key", nullable = false, unique = true) | ||
| 21 | + private String featureKey; | ||
| 22 | + | ||
| 23 | + @Column(name = "name", nullable = false) | ||
| 24 | + private String name; | ||
| 25 | + | ||
| 26 | + @Column(name = "description") | ||
| 27 | + private String description; | ||
| 28 | + | ||
| 29 | + @Column(name = "category") | ||
| 30 | + private String category; | ||
| 31 | + | ||
| 32 | + @Column(name = "is_premium") | ||
| 33 | + private Boolean isPremium; | ||
| 34 | + | ||
| 35 | + @Column(name = "sort_order") | ||
| 36 | + private Integer sortOrder; | ||
| 37 | + | ||
| 38 | + @Column(name = "is_active") | ||
| 39 | + private Boolean isActive; | ||
| 40 | + | ||
| 41 | + @Column(name = "created_at") | ||
| 42 | + private LocalDateTime createdAt; | ||
| 43 | + | ||
| 44 | + @Column(name = "updated_at") | ||
| 45 | + private LocalDateTime updatedAt; | ||
| 46 | + | ||
| 47 | + @PrePersist | ||
| 48 | + protected void onCreate() { | ||
| 49 | + createdAt = LocalDateTime.now(); | ||
| 50 | + updatedAt = LocalDateTime.now(); | ||
| 51 | + if (isActive == null) { | ||
| 52 | + isActive = true; | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @PreUpdate | ||
| 57 | + protected void onUpdate() { | ||
| 58 | + updatedAt = LocalDateTime.now(); | ||
| 59 | + } | ||
| 60 | +} |
| 1 | +package com.aigeo.ai.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.Feature; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Optional; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * AI功能模块仓库接口 | ||
| 12 | + */ | ||
| 13 | +@Repository | ||
| 14 | +public interface FeatureRepository extends JpaRepository<Feature, Integer> { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 根据功能标识符查找功能 | ||
| 18 | + */ | ||
| 19 | + Optional<Feature> findByFeatureKey(String featureKey); | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 根据分类查找功能列表 | ||
| 23 | + */ | ||
| 24 | + List<Feature> findByCategory(String category); | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 查找启用的功能列表 | ||
| 28 | + */ | ||
| 29 | + List<Feature> findByIsActiveTrue(); | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 根据分类和启用状态查找功能列表 | ||
| 33 | + */ | ||
| 34 | + List<Feature> findByCategoryAndIsActiveTrue(String category); | ||
| 35 | +} |
| 1 | +package com.aigeo.ai.service; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.Feature; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Optional; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * AI功能模块服务接口 | ||
| 10 | + */ | ||
| 11 | +public interface FeatureService { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 保存功能 | ||
| 15 | + */ | ||
| 16 | + Feature save(Feature feature); | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 根据ID查找功能 | ||
| 20 | + */ | ||
| 21 | + Optional<Feature> findById(Integer id); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 根据功能标识符查找功能 | ||
| 25 | + */ | ||
| 26 | + Optional<Feature> findByFeatureKey(String featureKey); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 查找所有功能 | ||
| 30 | + */ | ||
| 31 | + List<Feature> findAll(); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 查找启用的功能列表 | ||
| 35 | + */ | ||
| 36 | + List<Feature> findActiveFeatures(); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 根据分类查找功能列表 | ||
| 40 | + */ | ||
| 41 | + List<Feature> findByCategory(String category); | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 根据分类和启用状态查找功能列表 | ||
| 45 | + */ | ||
| 46 | + List<Feature> findByCategoryAndIsActiveTrue(String category); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 删除功能 | ||
| 50 | + */ | ||
| 51 | + void deleteById(Integer id); | ||
| 52 | +} |
| 1 | +package com.aigeo.ai.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
| 4 | +import com.aigeo.ai.repository.DifyApiConfigRepository; | ||
| 5 | +import com.aigeo.ai.service.DifyApiConfigService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * AI配置服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class DifyApiConfigServiceImpl implements DifyApiConfigService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private DifyApiConfigRepository difyApiConfigRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public DifyApiConfig save(DifyApiConfig config) { | ||
| 23 | + return difyApiConfigRepository.save(config); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<DifyApiConfig> findById(Integer id) { | ||
| 28 | + return difyApiConfigRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<DifyApiConfig> findByCompanyId(Integer companyId) { | ||
| 33 | + return difyApiConfigRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<DifyApiConfig> findActiveByCompanyId(Integer companyId) { | ||
| 38 | + return difyApiConfigRepository.findByCompanyIdAndIsActiveTrue(companyId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<DifyApiConfig> findByProvider(DifyApiConfig.Provider provider) { | ||
| 43 | + return difyApiConfigRepository.findByProvider(provider); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<DifyApiConfig> findActiveByProvider(DifyApiConfig.Provider provider) { | ||
| 48 | + return difyApiConfigRepository.findByProviderAndIsActiveTrue(provider); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<DifyApiConfig> findAll() { | ||
| 53 | + return difyApiConfigRepository.findAll(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void deleteById(Integer id) { | ||
| 58 | + difyApiConfigRepository.deleteById(id); | ||
| 59 | + } | ||
| 60 | +} |
| 1 | +package com.aigeo.ai.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.Feature; | ||
| 4 | +import com.aigeo.ai.repository.FeatureRepository; | ||
| 5 | +import com.aigeo.ai.service.FeatureService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * AI功能模块服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class FeatureServiceImpl implements FeatureService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private FeatureRepository featureRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public Feature save(Feature feature) { | ||
| 23 | + return featureRepository.save(feature); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<Feature> findById(Integer id) { | ||
| 28 | + return featureRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public Optional<Feature> findByFeatureKey(String featureKey) { | ||
| 33 | + return featureRepository.findByFeatureKey(featureKey); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<Feature> findAll() { | ||
| 38 | + return featureRepository.findAll(); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<Feature> findActiveFeatures() { | ||
| 43 | + return featureRepository.findByIsActiveTrue(); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<Feature> findByCategory(String category) { | ||
| 48 | + return featureRepository.findByCategory(category); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<Feature> findByCategoryAndIsActiveTrue(String category) { | ||
| 53 | + return featureRepository.findByCategoryAndIsActiveTrue(category); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void deleteById(Integer id) { | ||
| 58 | + featureRepository.deleteById(id); | ||
| 59 | + } | ||
| 60 | +} |
| 1 | +package com.aigeo.article.controller; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | +import com.aigeo.article.service.GeneratedArticleService; | ||
| 5 | +import com.aigeo.common.Result; | ||
| 6 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 7 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 10 | +import org.springframework.web.bind.annotation.*; | ||
| 11 | + | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 生成的文章控制器 | ||
| 16 | + */ | ||
| 17 | +@Slf4j | ||
| 18 | +@RestController | ||
| 19 | +@RequestMapping("/api/articles") | ||
| 20 | +@Tag(name = "生成文章管理", description = "AI生成文章相关接口") | ||
| 21 | +public class GeneratedArticleController { | ||
| 22 | + | ||
| 23 | + @Autowired | ||
| 24 | + private GeneratedArticleService generatedArticleService; | ||
| 25 | + | ||
| 26 | + @PostMapping | ||
| 27 | + @Operation(summary = "创建生成文章", description = "创建新的生成文章") | ||
| 28 | + public Result<GeneratedArticle> createArticle(@RequestBody GeneratedArticle article) { | ||
| 29 | + try { | ||
| 30 | + GeneratedArticle savedArticle = generatedArticleService.save(article); | ||
| 31 | + return Result.success("文章创建成功", savedArticle); | ||
| 32 | + } catch (Exception e) { | ||
| 33 | + log.error("创建生成文章失败", e); | ||
| 34 | + return Result.error("文章创建失败"); | ||
| 35 | + } | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + @GetMapping("/{id}") | ||
| 39 | + @Operation(summary = "获取生成文章详情", description = "根据ID获取生成文章详情") | ||
| 40 | + public Result<GeneratedArticle> getArticleById(@PathVariable Integer id) { | ||
| 41 | + try { | ||
| 42 | + return generatedArticleService.findById(id) | ||
| 43 | + .map(article -> Result.success("查询成功", article)) | ||
| 44 | + .orElse(Result.error("文章不存在")); | ||
| 45 | + } catch (Exception e) { | ||
| 46 | + log.error("获取生成文章详情失败, id: {}", id, e); | ||
| 47 | + return Result.error("查询失败"); | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @GetMapping | ||
| 52 | + @Operation(summary = "获取生成文章列表", description = "获取所有生成文章列表") | ||
| 53 | + public Result<List<GeneratedArticle>> getAllArticles() { | ||
| 54 | + try { | ||
| 55 | + List<GeneratedArticle> articles = generatedArticleService.findAll(); | ||
| 56 | + return Result.success("查询成功", articles); | ||
| 57 | + } catch (Exception e) { | ||
| 58 | + log.error("获取生成文章列表失败", e); | ||
| 59 | + return Result.error("查询失败"); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + @GetMapping("/task/{taskId}") | ||
| 64 | + @Operation(summary = "根据任务ID获取生成文章列表", description = "根据任务ID获取生成文章列表") | ||
| 65 | + public Result<List<GeneratedArticle>> getArticlesByTaskId(@PathVariable Integer taskId) { | ||
| 66 | + try { | ||
| 67 | + List<GeneratedArticle> articles = generatedArticleService.findByTaskId(taskId); | ||
| 68 | + return Result.success("查询成功", articles); | ||
| 69 | + } catch (Exception e) { | ||
| 70 | + log.error("根据任务ID获取生成文章列表失败, taskId: {}", taskId, e); | ||
| 71 | + return Result.error("查询失败"); | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + @GetMapping("/company/{companyId}") | ||
| 76 | + @Operation(summary = "根据公司ID获取生成文章列表", description = "根据公司ID获取生成文章列表") | ||
| 77 | + public Result<List<GeneratedArticle>> getArticlesByCompanyId(@PathVariable Integer companyId) { | ||
| 78 | + try { | ||
| 79 | + List<GeneratedArticle> articles = generatedArticleService.findByCompanyId(companyId); | ||
| 80 | + return Result.success("查询成功", articles); | ||
| 81 | + } catch (Exception e) { | ||
| 82 | + log.error("根据公司ID获取生成文章列表失败, companyId: {}", companyId, e); | ||
| 83 | + return Result.error("查询失败"); | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @GetMapping("/status/{status}") | ||
| 88 | + @Operation(summary = "根据状态获取生成文章列表", description = "根据状态获取生成文章列表") | ||
| 89 | + public Result<List<GeneratedArticle>> getArticlesByStatus(@PathVariable String status) { | ||
| 90 | + try { | ||
| 91 | + GeneratedArticle.ArticleStatus articleStatus = GeneratedArticle.ArticleStatus.fromCode(status); | ||
| 92 | + List<GeneratedArticle> articles = generatedArticleService.findByStatus(articleStatus); | ||
| 93 | + return Result.success("查询成功", articles); | ||
| 94 | + } catch (Exception e) { | ||
| 95 | + log.error("根据状态获取生成文章列表失败, status: {}", status, e); | ||
| 96 | + return Result.error("查询失败"); | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @PutMapping("/{id}") | ||
| 101 | + @Operation(summary = "更新生成文章", description = "更新生成文章信息") | ||
| 102 | + public Result<GeneratedArticle> updateArticle(@PathVariable Integer id, @RequestBody GeneratedArticle article) { | ||
| 103 | + try { | ||
| 104 | + if (!generatedArticleService.findById(id).isPresent()) { | ||
| 105 | + return Result.error("文章不存在"); | ||
| 106 | + } | ||
| 107 | + article.setId(id); | ||
| 108 | + GeneratedArticle updatedArticle = generatedArticleService.save(article); | ||
| 109 | + return Result.success("文章更新成功", updatedArticle); | ||
| 110 | + } catch (Exception e) { | ||
| 111 | + log.error("更新生成文章失败, id: {}", id, e); | ||
| 112 | + return Result.error("文章更新失败"); | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + @DeleteMapping("/{id}") | ||
| 117 | + @Operation(summary = "删除生成文章", description = "删除指定ID的生成文章") | ||
| 118 | + public Result<String> deleteArticle(@PathVariable Integer id) { | ||
| 119 | + try { | ||
| 120 | + if (!generatedArticleService.findById(id).isPresent()) { | ||
| 121 | + return Result.error("文章不存在"); | ||
| 122 | + } | ||
| 123 | + generatedArticleService.deleteById(id); | ||
| 124 | + return Result.success("文章删除成功"); | ||
| 125 | + } catch (Exception e) { | ||
| 126 | + log.error("删除生成文章失败, id: {}", id, e); | ||
| 127 | + return Result.error("文章删除失败"); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | +} |
| 1 | +package com.aigeo.article.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import org.springframework.beans.BeanUtils; | ||
| 7 | + | ||
| 8 | +import java.time.LocalDateTime; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 文章生成任务DTO | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@Schema(description = "文章生成任务DTO") | ||
| 15 | +public class ArticleGenerationTaskDTO { | ||
| 16 | + | ||
| 17 | + @Schema(description = "任务ID") | ||
| 18 | + private Integer id; | ||
| 19 | + | ||
| 20 | + @Schema(description = "公司ID") | ||
| 21 | + private Integer companyId; | ||
| 22 | + | ||
| 23 | + @Schema(description = "任务名称") | ||
| 24 | + private String name; | ||
| 25 | + | ||
| 26 | + @Schema(description = "主题") | ||
| 27 | + private String topic; | ||
| 28 | + | ||
| 29 | + @Schema(description = "关键词") | ||
| 30 | + private String keywords; | ||
| 31 | + | ||
| 32 | + @Schema(description = "文章数量") | ||
| 33 | + private Integer articleCount; | ||
| 34 | + | ||
| 35 | + @Schema(description = "平台类型") | ||
| 36 | + private String platformType; | ||
| 37 | + | ||
| 38 | + @Schema(description = "状态") | ||
| 39 | + private String status; | ||
| 40 | + | ||
| 41 | + @Schema(description = "创建时间") | ||
| 42 | + private LocalDateTime createdAt; | ||
| 43 | + | ||
| 44 | + @Schema(description = "更新时间") | ||
| 45 | + private LocalDateTime updatedAt; | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 将DTO转换为实体类 | ||
| 49 | + */ | ||
| 50 | + public ArticleGenerationTask toEntity() { | ||
| 51 | + ArticleGenerationTask entity = new ArticleGenerationTask(); | ||
| 52 | + BeanUtils.copyProperties(this, entity); | ||
| 53 | + return entity; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * 将实体类转换为DTO | ||
| 58 | + */ | ||
| 59 | + public static ArticleGenerationTaskDTO fromEntity(ArticleGenerationTask entity) { | ||
| 60 | + ArticleGenerationTaskDTO dto = new ArticleGenerationTaskDTO(); | ||
| 61 | + BeanUtils.copyProperties(entity, dto); | ||
| 62 | + return dto; | ||
| 63 | + } | ||
| 64 | +} |
| 1 | +package com.aigeo.article.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import org.springframework.beans.BeanUtils; | ||
| 7 | + | ||
| 8 | +import java.time.LocalDateTime; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 生成文章DTO | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@Schema(description = "生成文章DTO") | ||
| 15 | +public class GeneratedArticleDTO { | ||
| 16 | + | ||
| 17 | + @Schema(description = "文章ID") | ||
| 18 | + private Integer id; | ||
| 19 | + | ||
| 20 | + @Schema(description = "任务ID") | ||
| 21 | + private Integer taskId; | ||
| 22 | + | ||
| 23 | + @Schema(description = "公司ID") | ||
| 24 | + private Integer companyId; | ||
| 25 | + | ||
| 26 | + @Schema(description = "标题") | ||
| 27 | + private String title; | ||
| 28 | + | ||
| 29 | + @Schema(description = "内容") | ||
| 30 | + private String content; | ||
| 31 | + | ||
| 32 | + @Schema(description = "状态") | ||
| 33 | + private String status; | ||
| 34 | + | ||
| 35 | + @Schema(description = "创建时间") | ||
| 36 | + private LocalDateTime createdAt; | ||
| 37 | + | ||
| 38 | + @Schema(description = "更新时间") | ||
| 39 | + private LocalDateTime updatedAt; | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 将DTO转换为实体类 | ||
| 43 | + */ | ||
| 44 | + public GeneratedArticle toEntity() { | ||
| 45 | + GeneratedArticle entity = new GeneratedArticle(); | ||
| 46 | + BeanUtils.copyProperties(this, entity); | ||
| 47 | + return entity; | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 将实体类转换为DTO | ||
| 52 | + */ | ||
| 53 | + public static GeneratedArticleDTO fromEntity(GeneratedArticle entity) { | ||
| 54 | + GeneratedArticleDTO dto = new GeneratedArticleDTO(); | ||
| 55 | + BeanUtils.copyProperties(entity, dto); | ||
| 56 | + return dto; | ||
| 57 | + } | ||
| 58 | +} |
| 1 | +package com.aigeo.article.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 生成的文章仓库接口 | ||
| 11 | + */ | ||
| 12 | +@Repository | ||
| 13 | +public interface GeneratedArticleRepository extends JpaRepository<GeneratedArticle, Integer> { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 根据任务ID查找文章列表 | ||
| 17 | + */ | ||
| 18 | + List<GeneratedArticle> findByTaskId(Integer taskId); | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 根据公司ID查找文章列表 | ||
| 22 | + */ | ||
| 23 | + List<GeneratedArticle> findByCompanyId(Integer companyId); | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 根据任务ID和版本查找文章 | ||
| 27 | + */ | ||
| 28 | + GeneratedArticle findByTaskIdAndVersion(Integer taskId, Integer version); | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 根据任务ID查找选定的文章 | ||
| 32 | + */ | ||
| 33 | + GeneratedArticle findByTaskIdAndIsSelectedTrue(Integer taskId); | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 根据状态查找文章列表 | ||
| 37 | + */ | ||
| 38 | + List<GeneratedArticle> findByStatus(GeneratedArticle.ArticleStatus status); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 根据公司ID和状态查找文章列表 | ||
| 42 | + */ | ||
| 43 | + List<GeneratedArticle> findByCompanyIdAndStatus(Integer companyId, GeneratedArticle.ArticleStatus status); | ||
| 44 | +} |
| 1 | +package com.aigeo.article.service; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Optional; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 文章生成任务服务接口 | ||
| 10 | + */ | ||
| 11 | +public interface ArticleGenerationTaskService { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 保存任务 | ||
| 15 | + */ | ||
| 16 | + ArticleGenerationTask save(ArticleGenerationTask task); | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 根据ID查找任务 | ||
| 20 | + */ | ||
| 21 | + Optional<ArticleGenerationTask> findById(Integer id); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 根据公司ID查找任务列表 | ||
| 25 | + */ | ||
| 26 | + List<ArticleGenerationTask> findByCompanyId(Integer companyId); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 根据用户ID查找任务列表 | ||
| 30 | + */ | ||
| 31 | + List<ArticleGenerationTask> findByUserId(Integer userId); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 根据状态查找任务列表 | ||
| 35 | + */ | ||
| 36 | + List<ArticleGenerationTask> findByStatus(ArticleGenerationTask.TaskStatus status); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 根据公司ID和状态查找任务列表 | ||
| 40 | + */ | ||
| 41 | + List<ArticleGenerationTask> findByCompanyIdAndStatus(Integer companyId, ArticleGenerationTask.TaskStatus status); | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 根据用户ID和状态查找任务列表 | ||
| 45 | + */ | ||
| 46 | + List<ArticleGenerationTask> findByUserIdAndStatus(Integer userId, ArticleGenerationTask.TaskStatus status); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 查找所有任务 | ||
| 50 | + */ | ||
| 51 | + List<ArticleGenerationTask> findAll(); | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 删除任务 | ||
| 55 | + */ | ||
| 56 | + void deleteById(Integer id); | ||
| 57 | +} |
| 1 | +package com.aigeo.article.service; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Optional; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 生成的文章服务接口 | ||
| 10 | + */ | ||
| 11 | +public interface GeneratedArticleService { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 保存文章 | ||
| 15 | + */ | ||
| 16 | + GeneratedArticle save(GeneratedArticle article); | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 根据ID查找文章 | ||
| 20 | + */ | ||
| 21 | + Optional<GeneratedArticle> findById(Integer id); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 根据任务ID查找文章列表 | ||
| 25 | + */ | ||
| 26 | + List<GeneratedArticle> findByTaskId(Integer taskId); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 根据公司ID查找文章列表 | ||
| 30 | + */ | ||
| 31 | + List<GeneratedArticle> findByCompanyId(Integer companyId); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 根据任务ID和版本查找文章 | ||
| 35 | + */ | ||
| 36 | + GeneratedArticle findByTaskIdAndVersion(Integer taskId, Integer version); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 根据任务ID查找选定的文章 | ||
| 40 | + */ | ||
| 41 | + GeneratedArticle findByTaskIdAndIsSelectedTrue(Integer taskId); | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 根据状态查找文章列表 | ||
| 45 | + */ | ||
| 46 | + List<GeneratedArticle> findByStatus(GeneratedArticle.ArticleStatus status); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 根据公司ID和状态查找文章列表 | ||
| 50 | + */ | ||
| 51 | + List<GeneratedArticle> findByCompanyIdAndStatus(Integer companyId, GeneratedArticle.ArticleStatus status); | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 查找所有文章 | ||
| 55 | + */ | ||
| 56 | + List<GeneratedArticle> findAll(); | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 删除文章 | ||
| 60 | + */ | ||
| 61 | + void deleteById(Integer id); | ||
| 62 | +} |
| 1 | +package com.aigeo.article.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
| 4 | +import com.aigeo.article.repository.ArticleGenerationTaskRepository; | ||
| 5 | +import com.aigeo.article.service.ArticleGenerationTaskService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 文章生成任务服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class ArticleGenerationTaskServiceImpl implements ArticleGenerationTaskService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private ArticleGenerationTaskRepository articleGenerationTaskRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public ArticleGenerationTask save(ArticleGenerationTask task) { | ||
| 23 | + return articleGenerationTaskRepository.save(task); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<ArticleGenerationTask> findById(Integer id) { | ||
| 28 | + return articleGenerationTaskRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<ArticleGenerationTask> findByCompanyId(Integer companyId) { | ||
| 33 | + return articleGenerationTaskRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<ArticleGenerationTask> findByUserId(Integer userId) { | ||
| 38 | + return articleGenerationTaskRepository.findByUserId(userId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<ArticleGenerationTask> findByStatus(ArticleGenerationTask.TaskStatus status) { | ||
| 43 | + return articleGenerationTaskRepository.findByStatus(status); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<ArticleGenerationTask> findByCompanyIdAndStatus(Integer companyId, ArticleGenerationTask.TaskStatus status) { | ||
| 48 | + return articleGenerationTaskRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<ArticleGenerationTask> findByUserIdAndStatus(Integer userId, ArticleGenerationTask.TaskStatus status) { | ||
| 53 | + return articleGenerationTaskRepository.findByUserIdAndStatus(userId, status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<ArticleGenerationTask> findAll() { | ||
| 58 | + return articleGenerationTaskRepository.findAll(); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public void deleteById(Integer id) { | ||
| 63 | + articleGenerationTaskRepository.deleteById(id); | ||
| 64 | + } | ||
| 65 | +} |
| 1 | +package com.aigeo.article.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | +import com.aigeo.article.repository.GeneratedArticleRepository; | ||
| 5 | +import com.aigeo.article.service.GeneratedArticleService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 生成的文章服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class GeneratedArticleServiceImpl implements GeneratedArticleService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private GeneratedArticleRepository generatedArticleRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public GeneratedArticle save(GeneratedArticle article) { | ||
| 23 | + return generatedArticleRepository.save(article); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<GeneratedArticle> findById(Integer id) { | ||
| 28 | + return generatedArticleRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<GeneratedArticle> findByTaskId(Integer taskId) { | ||
| 33 | + return generatedArticleRepository.findByTaskId(taskId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<GeneratedArticle> findByCompanyId(Integer companyId) { | ||
| 38 | + return generatedArticleRepository.findByCompanyId(companyId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public GeneratedArticle findByTaskIdAndVersion(Integer taskId, Integer version) { | ||
| 43 | + return generatedArticleRepository.findByTaskIdAndVersion(taskId, version); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public GeneratedArticle findByTaskIdAndIsSelectedTrue(Integer taskId) { | ||
| 48 | + return generatedArticleRepository.findByTaskIdAndIsSelectedTrue(taskId); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<GeneratedArticle> findByStatus(GeneratedArticle.ArticleStatus status) { | ||
| 53 | + return generatedArticleRepository.findByStatus(status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<GeneratedArticle> findByCompanyIdAndStatus(Integer companyId, GeneratedArticle.ArticleStatus status) { | ||
| 58 | + return generatedArticleRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public List<GeneratedArticle> findAll() { | ||
| 63 | + return generatedArticleRepository.findAll(); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Override | ||
| 67 | + public void deleteById(Integer id) { | ||
| 68 | + generatedArticleRepository.deleteById(id); | ||
| 69 | + } | ||
| 70 | +} |
| 1 | +package com.aigeo.auth.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.company.entity.User; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 用户注册响应DTO | ||
| 9 | + */ | ||
| 10 | +@Data | ||
| 11 | +@Schema(description = "用户注册响应数据") | ||
| 12 | +public class RegisterResponse { | ||
| 13 | + | ||
| 14 | + @Schema(description = "访问令牌") | ||
| 15 | + private String token; | ||
| 16 | + | ||
| 17 | + @Schema(description = "过期时间(毫秒)") | ||
| 18 | + private Long expiresIn; | ||
| 19 | + | ||
| 20 | + @Schema(description = "用户信息") | ||
| 21 | + private User user; | ||
| 22 | +} |
| 1 | +package com.aigeo.auth.service; | ||
| 2 | + | ||
| 3 | +import com.aigeo.company.entity.User; | ||
| 4 | +import com.aigeo.company.service.UserService; | ||
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 6 | +import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| 7 | +import org.springframework.security.core.userdetails.UserDetails; | ||
| 8 | +import org.springframework.security.core.userdetails.UserDetailsService; | ||
| 9 | +import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
| 10 | +import org.springframework.stereotype.Service; | ||
| 11 | + | ||
| 12 | +import java.util.Collections; | ||
| 13 | +import java.util.Optional; | ||
| 14 | + | ||
| 15 | +@Service | ||
| 16 | +public class CustomUserDetailsService implements UserDetailsService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private UserService userService; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | ||
| 23 | + Optional<User> userOptional = userService.findByUsername(username); | ||
| 24 | + if (!userOptional.isPresent()) { | ||
| 25 | + userOptional = userService.findByEmail(username); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + if (!userOptional.isPresent()) { | ||
| 29 | + throw new UsernameNotFoundException("User not found with username or email: " + username); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + User user = userOptional.get(); | ||
| 33 | + return new org.springframework.security.core.userdetails.User( | ||
| 34 | + user.getUsername(), | ||
| 35 | + user.getPasswordHash(), | ||
| 36 | + user.getIsActive(), | ||
| 37 | + true, | ||
| 38 | + true, | ||
| 39 | + true, | ||
| 40 | + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) | ||
| 41 | + ); | ||
| 42 | + } | ||
| 43 | +} |
| 1 | +package com.aigeo.auth.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.auth.dto.LoginRequest; | ||
| 4 | +import com.aigeo.auth.dto.LoginResponse; | ||
| 5 | +import com.aigeo.auth.dto.RegisterRequest; | ||
| 6 | +import com.aigeo.auth.dto.RegisterResponse; | ||
| 7 | +import com.aigeo.auth.service.AuthService; | ||
| 8 | +import com.aigeo.company.entity.Company; | ||
| 9 | +import com.aigeo.company.entity.User; | ||
| 10 | +import com.aigeo.company.service.CompanyService; | ||
| 11 | +import com.aigeo.company.service.UserService; | ||
| 12 | +import com.aigeo.common.enums.CompanyStatus; | ||
| 13 | +import com.aigeo.common.enums.UserRole; | ||
| 14 | +import com.aigeo.common.exception.BusinessException; | ||
| 15 | +import com.aigeo.util.JwtUtil; | ||
| 16 | +import lombok.extern.slf4j.Slf4j; | ||
| 17 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 18 | +import org.springframework.security.crypto.password.PasswordEncoder; | ||
| 19 | +import org.springframework.stereotype.Service; | ||
| 20 | + | ||
| 21 | +import java.time.LocalDateTime; | ||
| 22 | +import java.util.Optional; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 认证服务实现类 | ||
| 26 | + */ | ||
| 27 | +@Slf4j | ||
| 28 | +@Service | ||
| 29 | +public class AuthServiceImpl implements AuthService { | ||
| 30 | + | ||
| 31 | + private final UserService userService; | ||
| 32 | + private final CompanyService companyService; | ||
| 33 | + private final PasswordEncoder passwordEncoder; | ||
| 34 | + private final JwtUtil jwtUtil; | ||
| 35 | + | ||
| 36 | + public AuthServiceImpl(UserService userService, CompanyService companyService, | ||
| 37 | + PasswordEncoder passwordEncoder, JwtUtil jwtUtil) { | ||
| 38 | + this.userService = userService; | ||
| 39 | + this.companyService = companyService; | ||
| 40 | + this.passwordEncoder = passwordEncoder; | ||
| 41 | + this.jwtUtil = jwtUtil; | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @Override | ||
| 45 | + public LoginResponse login(LoginRequest loginRequest) { | ||
| 46 | + try { | ||
| 47 | + // 查找用户 | ||
| 48 | + Optional<User> userOptional = userService.findByUsername(loginRequest.getUsername()); | ||
| 49 | + if (!userOptional.isPresent()) { | ||
| 50 | + // 尝试通过邮箱查找 | ||
| 51 | + userOptional = userService.findByEmail(loginRequest.getUsername()); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + if (!userOptional.isPresent()) { | ||
| 55 | + throw new BusinessException(400, "用户名或密码错误"); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + User user = userOptional.get(); | ||
| 59 | + | ||
| 60 | + // 验证密码 | ||
| 61 | + if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPasswordHash())) { | ||
| 62 | + throw new BusinessException(400, "用户名或密码错误"); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // 生成JWT token | ||
| 66 | + String token = jwtUtil.generateToken(user); | ||
| 67 | + | ||
| 68 | + // 构建响应 | ||
| 69 | + LoginResponse response = new LoginResponse(); | ||
| 70 | + response.setToken(token); | ||
| 71 | + response.setExpiresIn(jwtUtil.getExpirationTimeSeconds()); | ||
| 72 | + response.setUser(user); | ||
| 73 | + | ||
| 74 | + return response; | ||
| 75 | + } catch (Exception e) { | ||
| 76 | + log.error("用户登录失败: {}", loginRequest.getUsername(), e); | ||
| 77 | + throw new BusinessException(500, "登录失败"); | ||
| 78 | + } | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + @Override | ||
| 82 | + public RegisterResponse register(RegisterRequest registerRequest) { | ||
| 83 | + try { | ||
| 84 | + // 检查用户名是否已存在 | ||
| 85 | + if (userService.findByUsername(registerRequest.getUsername()).isPresent()) { | ||
| 86 | + throw new BusinessException(400, "用户名已存在"); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // 检查邮箱是否已存在 | ||
| 90 | + if (userService.findByEmail(registerRequest.getEmail()).isPresent()) { | ||
| 91 | + throw new BusinessException(400, "邮箱已被注册"); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + // 验证密码和确认密码是否一致 | ||
| 95 | + if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { | ||
| 96 | + throw new BusinessException(400, "密码和确认密码不一致"); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + // 确定公司ID | ||
| 100 | + Integer companyId = registerRequest.getCompanyId(); | ||
| 101 | + if (companyId == null) { | ||
| 102 | + // 如果没有提供公司ID,则需要提供公司名称来创建新公司 | ||
| 103 | + if (registerRequest.getCompanyName() == null || registerRequest.getCompanyName().isEmpty()) { | ||
| 104 | + throw new BusinessException(400, "必须提供公司ID或公司名称"); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + // 创建新公司 | ||
| 108 | + Company company = new Company(); | ||
| 109 | + company.setName(registerRequest.getCompanyName()); | ||
| 110 | + company.setBillingEmail(registerRequest.getEmail()); | ||
| 111 | + company.setStatus(CompanyStatus.TRIAL); | ||
| 112 | + company.setTrialExpiryDate(LocalDateTime.now().plusDays(30)); // 30天试用期 | ||
| 113 | + | ||
| 114 | + Company savedCompany = companyService.save(company); | ||
| 115 | + companyId = savedCompany.getId(); | ||
| 116 | + log.info("为新用户 {} 创建了新公司 {}, 公司ID: {}", | ||
| 117 | + registerRequest.getUsername(), registerRequest.getCompanyName(), companyId); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + // 创建新用户 | ||
| 121 | + User user = new User(); | ||
| 122 | + user.setUsername(registerRequest.getUsername()); | ||
| 123 | + user.setEmail(registerRequest.getEmail()); | ||
| 124 | + user.setPasswordHash(passwordEncoder.encode(registerRequest.getPassword())); | ||
| 125 | + user.setFullName(registerRequest.getFullName()); | ||
| 126 | + user.setPhone(registerRequest.getPhone()); | ||
| 127 | + user.setCompanyId(companyId); | ||
| 128 | + user.setRole(UserRole.EDITOR); // 使用现有的枚举值 | ||
| 129 | + user.setAvatarUrl(registerRequest.getAvatarUrl()); | ||
| 130 | + user.setIsActive(true); | ||
| 131 | + | ||
| 132 | + // 保存用户 | ||
| 133 | + User savedUser = userService.save(user); | ||
| 134 | + | ||
| 135 | + // 生成JWT token | ||
| 136 | + String token = jwtUtil.generateToken(savedUser); | ||
| 137 | + | ||
| 138 | + // 构建响应 | ||
| 139 | + RegisterResponse response = new RegisterResponse(); | ||
| 140 | + response.setToken(token); | ||
| 141 | + response.setExpiresIn(jwtUtil.getExpirationTimeSeconds()); | ||
| 142 | + response.setUser(savedUser); | ||
| 143 | + | ||
| 144 | + return response; | ||
| 145 | + } catch (BusinessException e) { | ||
| 146 | + throw e; | ||
| 147 | + } catch (Exception e) { | ||
| 148 | + log.error("用户注册失败: {}", registerRequest.getUsername(), e); | ||
| 149 | + throw new BusinessException(500, "注册失败"); | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | +} |
src/main/java/com/aigeo/common/Result.java
0 → 100644
| 1 | +package com.aigeo.common; | ||
| 2 | + | ||
| 3 | +import com.aigeo.common.enums.ResultCode; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.io.Serializable; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 统一响应结果封装类 | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +public class Result<T> implements Serializable { | ||
| 13 | + | ||
| 14 | + private static final long serialVersionUID = 1L; | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 状态码 | ||
| 18 | + */ | ||
| 19 | + private int code; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 消息 | ||
| 23 | + */ | ||
| 24 | + private String message; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 数据 | ||
| 28 | + */ | ||
| 29 | + private T data; | ||
| 30 | + | ||
| 31 | + public Result() { | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + public Result(int code, String message, T data) { | ||
| 35 | + this.code = code; | ||
| 36 | + this.message = message; | ||
| 37 | + this.data = data; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 成功 | ||
| 42 | + */ | ||
| 43 | + public static <T> Result<T> success() { | ||
| 44 | + return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 成功 | ||
| 49 | + */ | ||
| 50 | + public static <T> Result<T> success(T data) { | ||
| 51 | + return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 成功 | ||
| 56 | + */ | ||
| 57 | + public static <T> Result<T> success(String message, T data) { | ||
| 58 | + return new Result<>(ResultCode.SUCCESS.getCode(), message, data); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + /** | ||
| 62 | + * 失败 | ||
| 63 | + */ | ||
| 64 | + public static <T> Result<T> error() { | ||
| 65 | + return new Result<>(ResultCode.INTERNAL_SERVER_ERROR.getCode(), ResultCode.INTERNAL_SERVER_ERROR.getMessage(), null); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * 失败 | ||
| 70 | + */ | ||
| 71 | + public static <T> Result<T> error(String message) { | ||
| 72 | + return new Result<>(ResultCode.INTERNAL_SERVER_ERROR.getCode(), message, null); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /** | ||
| 76 | + * 失败 | ||
| 77 | + */ | ||
| 78 | + public static <T> Result<T> error(ResultCode resultCode) { | ||
| 79 | + return new Result<>(resultCode.getCode(), resultCode.getMessage(), null); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 失败 | ||
| 84 | + */ | ||
| 85 | + public static <T> Result<T> error(int code, String message) { | ||
| 86 | + return new Result<>(code, message, null); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + /** | ||
| 90 | + * 失败 | ||
| 91 | + */ | ||
| 92 | + public static <T> Result<T> error(ResultCode resultCode, String message) { | ||
| 93 | + return new Result<>(resultCode.getCode(), message, null); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * 失败 | ||
| 98 | + */ | ||
| 99 | + public static <T> Result<T> error(int code, String message, T data) { | ||
| 100 | + return new Result<>(code, message, data); | ||
| 101 | + } | ||
| 102 | +} |
| 1 | +package com.aigeo.common; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 分页结果封装类 | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "分页结果封装类") | ||
| 13 | +public class ResultPage<T> { | ||
| 14 | + | ||
| 15 | + @Schema(description = "当前页码") | ||
| 16 | + private int pageNumber; | ||
| 17 | + | ||
| 18 | + @Schema(description = "每页记录数") | ||
| 19 | + private int pageSize; | ||
| 20 | + | ||
| 21 | + @Schema(description = "总记录数") | ||
| 22 | + private long total; | ||
| 23 | + | ||
| 24 | + @Schema(description = "总页数") | ||
| 25 | + private int totalPages; | ||
| 26 | + | ||
| 27 | + @Schema(description = "当前页数据") | ||
| 28 | + private List<T> items; | ||
| 29 | + | ||
| 30 | + public ResultPage() { | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + public ResultPage(int pageNumber, int pageSize, long total, List<T> items) { | ||
| 34 | + this.pageNumber = pageNumber; | ||
| 35 | + this.pageSize = pageSize; | ||
| 36 | + this.total = total; | ||
| 37 | + this.items = items; | ||
| 38 | + this.totalPages = (int) Math.ceil((double) total / pageSize); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 创建分页结果 | ||
| 43 | + */ | ||
| 44 | + public static <T> ResultPage<T> of(int pageNumber, int pageSize, long total, List<T> items) { | ||
| 45 | + return new ResultPage<>(pageNumber, pageSize, total, items); | ||
| 46 | + } | ||
| 47 | +} |
| 1 | +package com.aigeo.common.enums; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 响应结果码枚举 | ||
| 5 | + */ | ||
| 6 | +public enum ResultCode { | ||
| 7 | + SUCCESS(200, "操作成功"), | ||
| 8 | + BAD_REQUEST(400, "请求参数错误"), | ||
| 9 | + UNAUTHORIZED(401, "未授权"), | ||
| 10 | + FORBIDDEN(403, "禁止访问"), | ||
| 11 | + NOT_FOUND(404, "资源不存在"), | ||
| 12 | + INTERNAL_SERVER_ERROR(500, "服务器内部错误"); | ||
| 13 | + | ||
| 14 | + private final int code; | ||
| 15 | + private final String message; | ||
| 16 | + | ||
| 17 | + ResultCode(int code, String message) { | ||
| 18 | + this.code = code; | ||
| 19 | + this.message = message; | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + public int getCode() { | ||
| 23 | + return code; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + public String getMessage() { | ||
| 27 | + return message; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + public static ResultCode fromCode(Integer code) { | ||
| 31 | + if (code == null) { | ||
| 32 | + return INTERNAL_SERVER_ERROR; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + for (ResultCode resultCode : ResultCode.values()) { | ||
| 36 | + if (resultCode.getCode() == code) { | ||
| 37 | + return resultCode; | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + return INTERNAL_SERVER_ERROR; | ||
| 41 | + } | ||
| 42 | +} |
| 1 | +package com.aigeo.company.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.common.enums.CompanyStatus; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.time.LocalDateTime; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 公司DTO | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +@Schema(description = "公司数据传输对象") | ||
| 14 | +public class CompanyDTO { | ||
| 15 | + | ||
| 16 | + @Schema(description = "公司ID") | ||
| 17 | + private Integer id; | ||
| 18 | + | ||
| 19 | + @Schema(description = "公司名称") | ||
| 20 | + private String name; | ||
| 21 | + | ||
| 22 | + @Schema(description = "公司域名") | ||
| 23 | + private String domain; | ||
| 24 | + | ||
| 25 | + @Schema(description = "公司状态") | ||
| 26 | + private CompanyStatus status; | ||
| 27 | + | ||
| 28 | + @Schema(description = "试用到期日") | ||
| 29 | + private LocalDateTime trialExpiryDate; | ||
| 30 | + | ||
| 31 | + @Schema(description = "企业默认设置(JSON)") | ||
| 32 | + private String defaultSettings; | ||
| 33 | + | ||
| 34 | + @Schema(description = "账单邮箱") | ||
| 35 | + private String billingEmail; | ||
| 36 | + | ||
| 37 | + @Schema(description = "联系电话") | ||
| 38 | + private String contactPhone; | ||
| 39 | + | ||
| 40 | + @Schema(description = "公司地址") | ||
| 41 | + private String address; | ||
| 42 | + | ||
| 43 | + @Schema(description = "公司Logo URL") | ||
| 44 | + private String logoUrl; | ||
| 45 | + | ||
| 46 | + @Schema(description = "创建时间") | ||
| 47 | + private LocalDateTime createdAt; | ||
| 48 | + | ||
| 49 | + @Schema(description = "最后更新时间") | ||
| 50 | + private LocalDateTime updatedAt; | ||
| 51 | +} |
| 1 | +package com.aigeo.company.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.common.enums.UserRole; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.time.LocalDateTime; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 用户DTO | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +@Schema(description = "用户数据传输对象") | ||
| 14 | +public class UserDTO { | ||
| 15 | + | ||
| 16 | + @Schema(description = "用户ID") | ||
| 17 | + private Integer id; | ||
| 18 | + | ||
| 19 | + @Schema(description = "所属公司ID") | ||
| 20 | + private Integer companyId; | ||
| 21 | + | ||
| 22 | + @Schema(description = "用户名") | ||
| 23 | + private String username; | ||
| 24 | + | ||
| 25 | + @Schema(description = "用户邮箱") | ||
| 26 | + private String email; | ||
| 27 | + | ||
| 28 | + @Schema(description = "用户全名") | ||
| 29 | + private String fullName; | ||
| 30 | + | ||
| 31 | + @Schema(description = "用户头像URL") | ||
| 32 | + private String avatarUrl; | ||
| 33 | + | ||
| 34 | + @Schema(description = "手机号") | ||
| 35 | + private String phone; | ||
| 36 | + | ||
| 37 | + @Schema(description = "用户角色") | ||
| 38 | + private UserRole role; | ||
| 39 | + | ||
| 40 | + @Schema(description = "是否启用") | ||
| 41 | + private Boolean isActive; | ||
| 42 | + | ||
| 43 | + @Schema(description = "最近登录时间") | ||
| 44 | + private LocalDateTime lastLogin; | ||
| 45 | + | ||
| 46 | + @Schema(description = "上次修改密码时间") | ||
| 47 | + private LocalDateTime lastPasswordChange; | ||
| 48 | + | ||
| 49 | + @Schema(description = "登录失败次数") | ||
| 50 | + private Integer failedLoginAttempts; | ||
| 51 | + | ||
| 52 | + @Schema(description = "锁定截止时间") | ||
| 53 | + private LocalDateTime lockedUntil; | ||
| 54 | + | ||
| 55 | + @Schema(description = "用户时区") | ||
| 56 | + private String timezone; | ||
| 57 | + | ||
| 58 | + @Schema(description = "用户个性化设置") | ||
| 59 | + private String preferences; | ||
| 60 | + | ||
| 61 | + @Schema(description = "创建时间") | ||
| 62 | + private LocalDateTime createdAt; | ||
| 63 | + | ||
| 64 | + @Schema(description = "最后更新时间") | ||
| 65 | + private LocalDateTime updatedAt; | ||
| 66 | +} |
| 1 | +package com.aigeo.company.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.common.enums.CompanyStatus; | ||
| 4 | +import com.aigeo.company.entity.Company; | ||
| 5 | +import com.aigeo.company.repository.CompanyRepository; | ||
| 6 | +import com.aigeo.company.service.CompanyService; | ||
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | +import org.springframework.stereotype.Service; | ||
| 9 | + | ||
| 10 | +import java.util.List; | ||
| 11 | +import java.util.Optional; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * 公司服务实现类 | ||
| 15 | + */ | ||
| 16 | +@Service | ||
| 17 | +public class CompanyServiceImpl implements CompanyService { | ||
| 18 | + | ||
| 19 | + @Autowired | ||
| 20 | + private CompanyRepository companyRepository; | ||
| 21 | + | ||
| 22 | + @Override | ||
| 23 | + public Company save(Company company) { | ||
| 24 | + return companyRepository.save(company); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public Optional<Company> findById(Integer id) { | ||
| 29 | + return companyRepository.findById(id); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Override | ||
| 33 | + public List<Company> findByStatus(CompanyStatus status) { | ||
| 34 | + return companyRepository.findByStatus(status); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + @Override | ||
| 38 | + public List<Company> findByName(String name) { | ||
| 39 | + return companyRepository.findByName(name); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public List<Company> findByDomain(String domain) { | ||
| 44 | + return companyRepository.findByDomain(domain); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + @Override | ||
| 48 | + public List<Company> findAll() { | ||
| 49 | + return companyRepository.findAll(); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + @Override | ||
| 53 | + public void deleteById(Integer id) { | ||
| 54 | + companyRepository.deleteById(id); | ||
| 55 | + } | ||
| 56 | +} |
| 1 | +package com.aigeo.company.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.common.enums.UserRole; | ||
| 4 | +import com.aigeo.company.entity.User; | ||
| 5 | +import com.aigeo.company.repository.UserRepository; | ||
| 6 | +import com.aigeo.company.service.UserService; | ||
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | +import org.springframework.stereotype.Service; | ||
| 9 | + | ||
| 10 | +import java.util.List; | ||
| 11 | +import java.util.Optional; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * 用户服务实现类 | ||
| 15 | + */ | ||
| 16 | +@Service | ||
| 17 | +public class UserServiceImpl implements UserService { | ||
| 18 | + | ||
| 19 | + @Autowired | ||
| 20 | + private UserRepository userRepository; | ||
| 21 | + | ||
| 22 | + @Override | ||
| 23 | + public User save(User user) { | ||
| 24 | + return userRepository.save(user); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public Optional<User> findById(Integer id) { | ||
| 29 | + return userRepository.findById(id); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Override | ||
| 33 | + public Optional<User> findByUsername(String username) { | ||
| 34 | + return userRepository.findByUsername(username); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + @Override | ||
| 38 | + public Optional<User> findByEmail(String email) { | ||
| 39 | + return userRepository.findByEmail(email); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public List<User> findByCompanyId(Integer companyId) { | ||
| 44 | + return userRepository.findByCompanyId(companyId); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + @Override | ||
| 48 | + public List<User> findActiveByCompanyId(Integer companyId) { | ||
| 49 | + return userRepository.findByCompanyIdAndIsActiveTrue(companyId); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + @Override | ||
| 53 | + public List<User> findByRole(UserRole role) { | ||
| 54 | + return userRepository.findByRole(role); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + @Override | ||
| 58 | + public List<User> findByCompanyIdAndRole(Integer companyId, UserRole role) { | ||
| 59 | + return userRepository.findByCompanyIdAndRole(companyId, role); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @Override | ||
| 63 | + public List<User> findAll() { | ||
| 64 | + return userRepository.findAll(); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Override | ||
| 68 | + public void deleteById(Integer id) { | ||
| 69 | + userRepository.deleteById(id); | ||
| 70 | + } | ||
| 71 | +} |
| 1 | +package com.aigeo.config; | ||
| 2 | + | ||
| 3 | +import com.aigeo.auth.service.CustomUserDetailsService; | ||
| 4 | +import com.aigeo.util.JwtUtil; | ||
| 5 | +import jakarta.servlet.FilterChain; | ||
| 6 | +import jakarta.servlet.ServletException; | ||
| 7 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 8 | +import jakarta.servlet.http.HttpServletResponse; | ||
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 10 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| 11 | +import org.springframework.security.core.context.SecurityContextHolder; | ||
| 12 | +import org.springframework.security.core.userdetails.UserDetails; | ||
| 13 | +import org.springframework.security.core.userdetails.UserDetailsService; | ||
| 14 | +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
| 15 | +import org.springframework.stereotype.Component; | ||
| 16 | +import org.springframework.web.filter.OncePerRequestFilter; | ||
| 17 | + | ||
| 18 | +import java.io.IOException; | ||
| 19 | + | ||
| 20 | +@Component | ||
| 21 | +public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
| 22 | + | ||
| 23 | + @Autowired | ||
| 24 | + private JwtUtil jwtUtil; | ||
| 25 | + | ||
| 26 | + @Autowired | ||
| 27 | + private CustomUserDetailsService userDetailsService; | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | ||
| 31 | + throws ServletException, IOException { | ||
| 32 | + | ||
| 33 | + final String requestTokenHeader = request.getHeader("Authorization"); | ||
| 34 | + | ||
| 35 | + String username = null; | ||
| 36 | + String jwtToken = null; | ||
| 37 | + | ||
| 38 | + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { | ||
| 39 | + jwtToken = requestTokenHeader.substring(7); | ||
| 40 | + try { | ||
| 41 | + username = jwtUtil.getUsernameFromToken(jwtToken); | ||
| 42 | + } catch (Exception e) { | ||
| 43 | + logger.error("Unable to get JWT Token", e); | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { | ||
| 48 | + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); | ||
| 49 | + | ||
| 50 | + // 创建一个临时User对象用于验证token | ||
| 51 | + com.aigeo.company.entity.User tempUser = new com.aigeo.company.entity.User(); | ||
| 52 | + tempUser.setUsername(username); | ||
| 53 | + | ||
| 54 | + if (jwtUtil.validateToken(jwtToken, tempUser)) { | ||
| 55 | + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( | ||
| 56 | + userDetails, null, userDetails.getAuthorities()); | ||
| 57 | + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
| 58 | + SecurityContextHolder.getContext().setAuthentication(authToken); | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + chain.doFilter(request, response); | ||
| 62 | + } | ||
| 63 | +} |
| 1 | +package com.aigeo.config; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.models.Components; | ||
| 4 | +import io.swagger.v3.oas.models.OpenAPI; | ||
| 5 | +import io.swagger.v3.oas.models.info.Contact; | ||
| 6 | +import io.swagger.v3.oas.models.info.Info; | ||
| 7 | +import io.swagger.v3.oas.models.info.License; | ||
| 8 | +import io.swagger.v3.oas.models.security.SecurityRequirement; | ||
| 9 | +import io.swagger.v3.oas.models.security.SecurityScheme; | ||
| 10 | +import org.springframework.context.annotation.Bean; | ||
| 11 | +import org.springframework.context.annotation.Configuration; | ||
| 12 | + | ||
| 13 | +@Configuration | ||
| 14 | +public class Knife4jConfig { | ||
| 15 | + | ||
| 16 | + @Bean | ||
| 17 | + public OpenAPI customOpenAPI() { | ||
| 18 | + return new OpenAPI() | ||
| 19 | + .info(new Info() | ||
| 20 | + .title("AIGEO API Documentation") | ||
| 21 | + .version("1.0.0") | ||
| 22 | + .description("AI Content Generation Platform API Documentation") | ||
| 23 | + .contact(new Contact() | ||
| 24 | + .name("AIGEO Team") | ||
| 25 | + .url("https://www.aigeo.com") | ||
| 26 | + .email("contact@aigeo.com")) | ||
| 27 | + .license(new License() | ||
| 28 | + .name("Apache 2.0") | ||
| 29 | + .url("http://www.apache.org/licenses/LICENSE-2.0.html"))) | ||
| 30 | + .components(new Components() | ||
| 31 | + .addSecuritySchemes("bearerAuth", new SecurityScheme() | ||
| 32 | + .type(SecurityScheme.Type.HTTP) | ||
| 33 | + .scheme("bearer") | ||
| 34 | + .bearerFormat("JWT"))) | ||
| 35 | + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")); | ||
| 36 | + } | ||
| 37 | +} |
| 1 | +package com.aigeo.config; | ||
| 2 | + | ||
| 3 | +import org.springframework.context.annotation.Bean; | ||
| 4 | +import org.springframework.context.annotation.Configuration; | ||
| 5 | +import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
| 6 | +import org.springframework.data.redis.core.RedisTemplate; | ||
| 7 | +import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
| 8 | + | ||
| 9 | +@Configuration | ||
| 10 | +public class RedisConfig { | ||
| 11 | + | ||
| 12 | + /* | ||
| 13 | + @Bean | ||
| 14 | + public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { | ||
| 15 | + RedisTemplate<String, Object> template = new RedisTemplate<>(); | ||
| 16 | + template.setConnectionFactory(connectionFactory); | ||
| 17 | + template.setKeySerializer(new StringRedisSerializer()); | ||
| 18 | + template.setValueSerializer(new StringRedisSerializer()); | ||
| 19 | + return template; | ||
| 20 | + } | ||
| 21 | + */ | ||
| 22 | +} |
| 1 | +package com.aigeo.config; | ||
| 2 | + | ||
| 3 | +import com.aigeo.auth.service.CustomUserDetailsService; | ||
| 4 | +import com.aigeo.config.JwtAuthenticationFilter; | ||
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 6 | +import org.springframework.context.annotation.Bean; | ||
| 7 | +import org.springframework.context.annotation.Configuration; | ||
| 8 | +import org.springframework.security.authentication.AuthenticationManager; | ||
| 9 | +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; | ||
| 10 | +import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| 11 | +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
| 12 | +import org.springframework.security.config.http.SessionCreationPolicy; | ||
| 13 | +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
| 14 | +import org.springframework.security.crypto.password.PasswordEncoder; | ||
| 15 | +import org.springframework.security.web.SecurityFilterChain; | ||
| 16 | +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
| 17 | + | ||
| 18 | +@Configuration | ||
| 19 | +@EnableWebSecurity | ||
| 20 | +public class SecurityConfig { | ||
| 21 | + | ||
| 22 | + @Autowired | ||
| 23 | + private CustomUserDetailsService userDetailsService; | ||
| 24 | + | ||
| 25 | + @Autowired | ||
| 26 | + private JwtAuthenticationFilter jwtAuthenticationFilter; | ||
| 27 | + | ||
| 28 | + @Bean | ||
| 29 | + public PasswordEncoder passwordEncoder() { | ||
| 30 | + return new BCryptPasswordEncoder(); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + @Bean | ||
| 34 | + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { | ||
| 35 | + return config.getAuthenticationManager(); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + @Bean | ||
| 39 | + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
| 40 | + http.csrf(csrf -> csrf.disable()) | ||
| 41 | + .authorizeHttpRequests(authz -> authz | ||
| 42 | + .requestMatchers("/auth/**").permitAll() | ||
| 43 | + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", | ||
| 44 | + "/doc.html", "/webjars/**", "/swagger-resources/**").permitAll() | ||
| 45 | + .anyRequest().authenticated() | ||
| 46 | + ) | ||
| 47 | + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); | ||
| 48 | + | ||
| 49 | + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||
| 50 | + | ||
| 51 | + return http.build(); | ||
| 52 | + } | ||
| 53 | +} |
| 1 | +package com.aigeo.config; | ||
| 2 | + | ||
| 3 | +import org.springframework.context.annotation.Configuration; | ||
| 4 | +import org.springframework.web.servlet.config.annotation.CorsRegistry; | ||
| 5 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
| 6 | + | ||
| 7 | +@Configuration | ||
| 8 | +public class WebConfig implements WebMvcConfigurer { | ||
| 9 | + | ||
| 10 | + @Override | ||
| 11 | + public void addCorsMappings(CorsRegistry registry) { | ||
| 12 | + registry.addMapping("/api/**") | ||
| 13 | + .allowedOrigins("*") | ||
| 14 | + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") | ||
| 15 | + .allowedHeaders("*") | ||
| 16 | + .maxAge(3600); | ||
| 17 | + } | ||
| 18 | +} |
| 1 | +package com.aigeo.keyword.dto; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 关键词DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "关键词数据传输对象") | ||
| 13 | +public class KeywordDTO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "关键词ID") | ||
| 16 | + private Integer id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "公司ID") | ||
| 19 | + private Integer companyId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "关键词文本") | ||
| 22 | + private String keyword; | ||
| 23 | + | ||
| 24 | + @Schema(description = "搜索量估算") | ||
| 25 | + private Integer searchVolume; | ||
| 26 | + | ||
| 27 | + @Schema(description = "估计每次点击成本") | ||
| 28 | + private Double cpc; | ||
| 29 | + | ||
| 30 | + @Schema(description = "关键词难度评分") | ||
| 31 | + private Byte difficulty; | ||
| 32 | + | ||
| 33 | + @Schema(description = "来源类型") | ||
| 34 | + private String source; | ||
| 35 | + | ||
| 36 | + @Schema(description = "关键词状态") | ||
| 37 | + private String status; | ||
| 38 | + | ||
| 39 | + @Schema(description = "关键词标签") | ||
| 40 | + private String tags; | ||
| 41 | + | ||
| 42 | + @Schema(description = "关键词被文章使用次数") | ||
| 43 | + private Integer usageCount; | ||
| 44 | + | ||
| 45 | + @Schema(description = "创建时间") | ||
| 46 | + private LocalDateTime createdAt; | ||
| 47 | + | ||
| 48 | + @Schema(description = "更新时间") | ||
| 49 | + private LocalDateTime updatedAt; | ||
| 50 | +} |
| 1 | +package com.aigeo.keyword.dto; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 话题DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "话题数据传输对象") | ||
| 13 | +public class TopicDTO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "话题ID") | ||
| 16 | + private Integer id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "公司ID") | ||
| 19 | + private Integer companyId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "来源任务ID") | ||
| 22 | + private Integer sourceTaskId; | ||
| 23 | + | ||
| 24 | + @Schema(description = "话题标题") | ||
| 25 | + private String title; | ||
| 26 | + | ||
| 27 | + @Schema(description = "话题描述") | ||
| 28 | + private String description; | ||
| 29 | + | ||
| 30 | + @Schema(description = "原始来源链接") | ||
| 31 | + private String sourceUrl; | ||
| 32 | + | ||
| 33 | + @Schema(description = "话题状态") | ||
| 34 | + private String status; | ||
| 35 | + | ||
| 36 | + @Schema(description = "创建时间") | ||
| 37 | + private LocalDateTime createdAt; | ||
| 38 | + | ||
| 39 | + @Schema(description = "更新时间") | ||
| 40 | + private LocalDateTime updatedAt; | ||
| 41 | +} |
| 1 | +package com.aigeo.keyword.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.keyword.entity.Keyword; | ||
| 4 | +import com.aigeo.keyword.repository.KeywordRepository; | ||
| 5 | +import com.aigeo.keyword.service.KeywordService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 关键词服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class KeywordServiceImpl implements KeywordService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private KeywordRepository keywordRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public Keyword save(Keyword keyword) { | ||
| 23 | + return keywordRepository.save(keyword); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<Keyword> findById(Integer id) { | ||
| 28 | + return keywordRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<Keyword> findByCompanyId(Integer companyId) { | ||
| 33 | + return keywordRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<Keyword> findByKeyword(String keyword) { | ||
| 38 | + return keywordRepository.findByKeyword(keyword); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<Keyword> findByCompanyIdAndKeyword(Integer companyId, String keyword) { | ||
| 43 | + return keywordRepository.findByCompanyIdAndKeyword(companyId, keyword); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<Keyword> findByStatus(String status) { | ||
| 48 | + return keywordRepository.findByStatus(status); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<Keyword> findByCompanyIdAndStatus(Integer companyId, String status) { | ||
| 53 | + return keywordRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<Keyword> findByTagsContaining(String tag) { | ||
| 58 | + return keywordRepository.findByTagsContaining(tag); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public List<Keyword> findAll() { | ||
| 63 | + return keywordRepository.findAll(); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Override | ||
| 67 | + public void deleteById(Integer id) { | ||
| 68 | + keywordRepository.deleteById(id); | ||
| 69 | + } | ||
| 70 | +} |
| 1 | +package com.aigeo.keyword.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.keyword.entity.Topic; | ||
| 4 | +import com.aigeo.keyword.repository.TopicRepository; | ||
| 5 | +import com.aigeo.keyword.service.TopicService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 话题服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class TopicServiceImpl implements TopicService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private TopicRepository topicRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public Topic save(Topic topic) { | ||
| 23 | + return topicRepository.save(topic); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<Topic> findById(Integer id) { | ||
| 28 | + return topicRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<Topic> findByCompanyId(Integer companyId) { | ||
| 33 | + return topicRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<Topic> findBySourceTaskId(Integer sourceTaskId) { | ||
| 38 | + return topicRepository.findBySourceTaskId(sourceTaskId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<Topic> findByTitle(String title) { | ||
| 43 | + return topicRepository.findByTitle(title); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<Topic> findByCompanyIdAndTitle(Integer companyId, String title) { | ||
| 48 | + return topicRepository.findByCompanyIdAndTitle(companyId, title); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<Topic> findByStatus(String status) { | ||
| 53 | + return topicRepository.findByStatus(status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<Topic> findByCompanyIdAndStatus(Integer companyId, String status) { | ||
| 58 | + return topicRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public List<Topic> findAll() { | ||
| 63 | + return topicRepository.findAll(); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Override | ||
| 67 | + public void deleteById(Integer id) { | ||
| 68 | + topicRepository.deleteById(id); | ||
| 69 | + } | ||
| 70 | +} |
| 1 | +package com.aigeo.landingpage.controller; | ||
| 2 | + | ||
| 3 | +import com.aigeo.landingpage.entity.LandingPageProject; | ||
| 4 | +import com.aigeo.landingpage.service.LandingPageProjectService; | ||
| 5 | +import com.aigeo.common.Result; | ||
| 6 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 7 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 10 | +import org.springframework.web.bind.annotation.*; | ||
| 11 | + | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 落地页项目控制器 | ||
| 16 | + */ | ||
| 17 | +@Slf4j | ||
| 18 | +@RestController | ||
| 19 | +@RequestMapping("/api/landing-page-projects") | ||
| 20 | +@Tag(name = "落地页项目管理", description = "落地页项目相关接口") | ||
| 21 | +public class LandingPageProjectController { | ||
| 22 | + | ||
| 23 | + @Autowired | ||
| 24 | + private LandingPageProjectService landingPageProjectService; | ||
| 25 | + | ||
| 26 | + @PostMapping | ||
| 27 | + @Operation(summary = "创建落地页项目", description = "创建新的落地页项目") | ||
| 28 | + public Result<LandingPageProject> createProject(@RequestBody LandingPageProject project) { | ||
| 29 | + try { | ||
| 30 | + LandingPageProject savedProject = landingPageProjectService.save(project); | ||
| 31 | + return Result.success("项目创建成功", savedProject); | ||
| 32 | + } catch (Exception e) { | ||
| 33 | + log.error("创建落地页项目失败", e); | ||
| 34 | + return Result.error("项目创建失败"); | ||
| 35 | + } | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + @GetMapping("/{id}") | ||
| 39 | + @Operation(summary = "获取落地页项目详情", description = "根据ID获取落地页项目详情") | ||
| 40 | + public Result<LandingPageProject> getProjectById(@PathVariable Integer id) { | ||
| 41 | + try { | ||
| 42 | + return landingPageProjectService.findById(id) | ||
| 43 | + .map(project -> Result.success("查询成功", project)) | ||
| 44 | + .orElse(Result.error("项目不存在")); | ||
| 45 | + } catch (Exception e) { | ||
| 46 | + log.error("获取落地页项目详情失败, id: {}", id, e); | ||
| 47 | + return Result.error("查询失败"); | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @GetMapping | ||
| 52 | + @Operation(summary = "获取落地页项目列表", description = "获取所有落地页项目列表") | ||
| 53 | + public Result<List<LandingPageProject>> getAllProjects() { | ||
| 54 | + try { | ||
| 55 | + List<LandingPageProject> projects = landingPageProjectService.findAll(); | ||
| 56 | + return Result.success("查询成功", projects); | ||
| 57 | + } catch (Exception e) { | ||
| 58 | + log.error("获取落地页项目列表失败", e); | ||
| 59 | + return Result.error("查询失败"); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + @GetMapping("/company/{companyId}") | ||
| 64 | + @Operation(summary = "根据公司ID获取落地页项目列表", description = "根据公司ID获取落地页项目列表") | ||
| 65 | + public Result<List<LandingPageProject>> getProjectsByCompanyId(@PathVariable Integer companyId) { | ||
| 66 | + try { | ||
| 67 | + List<LandingPageProject> projects = landingPageProjectService.findByCompanyId(companyId); | ||
| 68 | + return Result.success("查询成功", projects); | ||
| 69 | + } catch (Exception e) { | ||
| 70 | + log.error("根据公司ID获取落地页项目列表失败, companyId: {}", companyId, e); | ||
| 71 | + return Result.error("查询失败"); | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + @GetMapping("/user/{userId}") | ||
| 76 | + @Operation(summary = "根据用户ID获取落地页项目列表", description = "根据用户ID获取落地页项目列表") | ||
| 77 | + public Result<List<LandingPageProject>> getProjectsByUserId(@PathVariable Integer userId) { | ||
| 78 | + try { | ||
| 79 | + List<LandingPageProject> projects = landingPageProjectService.findByUserId(userId); | ||
| 80 | + return Result.success("查询成功", projects); | ||
| 81 | + } catch (Exception e) { | ||
| 82 | + log.error("根据用户ID获取落地页项目列表失败, userId: {}", userId, e); | ||
| 83 | + return Result.error("查询失败"); | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @GetMapping("/status/{status}") | ||
| 88 | + @Operation(summary = "根据状态获取落地页项目列表", description = "根据状态获取落地页项目列表") | ||
| 89 | + public Result<List<LandingPageProject>> getProjectsByStatus(@PathVariable String status) { | ||
| 90 | + try { | ||
| 91 | + LandingPageProject.ProjectStatus projectStatus = LandingPageProject.ProjectStatus.fromCode(status); | ||
| 92 | + List<LandingPageProject> projects = landingPageProjectService.findByStatus(projectStatus); | ||
| 93 | + return Result.success("查询成功", projects); | ||
| 94 | + } catch (Exception e) { | ||
| 95 | + log.error("根据状态获取落地页项目列表失败, status: {}", status, e); | ||
| 96 | + return Result.error("查询失败"); | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @PutMapping("/{id}") | ||
| 101 | + @Operation(summary = "更新落地页项目", description = "更新落地页项目信息") | ||
| 102 | + public Result<LandingPageProject> updateProject(@PathVariable Integer id, @RequestBody LandingPageProject project) { | ||
| 103 | + try { | ||
| 104 | + if (!landingPageProjectService.findById(id).isPresent()) { | ||
| 105 | + return Result.error("项目不存在"); | ||
| 106 | + } | ||
| 107 | + project.setId(id); | ||
| 108 | + LandingPageProject updatedProject = landingPageProjectService.save(project); | ||
| 109 | + return Result.success("项目更新成功", updatedProject); | ||
| 110 | + } catch (Exception e) { | ||
| 111 | + log.error("更新落地页项目失败, id: {}", id, e); | ||
| 112 | + return Result.error("项目更新失败"); | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + @DeleteMapping("/{id}") | ||
| 117 | + @Operation(summary = "删除落地页项目", description = "删除指定ID的落地页项目") | ||
| 118 | + public Result<String> deleteProject(@PathVariable Integer id) { | ||
| 119 | + try { | ||
| 120 | + if (!landingPageProjectService.findById(id).isPresent()) { | ||
| 121 | + return Result.error("项目不存在"); | ||
| 122 | + } | ||
| 123 | + landingPageProjectService.deleteById(id); | ||
| 124 | + return Result.success("项目删除成功"); | ||
| 125 | + } catch (Exception e) { | ||
| 126 | + log.error("删除落地页项目失败, id: {}", id, e); | ||
| 127 | + return Result.error("项目删除失败"); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | +} |
| 1 | +package com.aigeo.landingpage.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.landingpage.entity.LandingPageProject; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import org.springframework.beans.BeanUtils; | ||
| 7 | + | ||
| 8 | +import java.time.LocalDateTime; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 落地页项目DTO | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@Schema(description = "落地页项目DTO") | ||
| 15 | +public class LandingPageProjectDTO { | ||
| 16 | + | ||
| 17 | + @Schema(description = "项目ID") | ||
| 18 | + private Integer id; | ||
| 19 | + | ||
| 20 | + @Schema(description = "公司ID") | ||
| 21 | + private Integer companyId; | ||
| 22 | + | ||
| 23 | + @Schema(description = "项目名称") | ||
| 24 | + private String name; | ||
| 25 | + | ||
| 26 | + @Schema(description = "域名") | ||
| 27 | + private String domain; | ||
| 28 | + | ||
| 29 | + @Schema(description = "状态") | ||
| 30 | + private String status; | ||
| 31 | + | ||
| 32 | + @Schema(description = "创建时间") | ||
| 33 | + private LocalDateTime createdAt; | ||
| 34 | + | ||
| 35 | + @Schema(description = "更新时间") | ||
| 36 | + private LocalDateTime updatedAt; | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 将DTO转换为实体类 | ||
| 40 | + */ | ||
| 41 | + public LandingPageProject toEntity() { | ||
| 42 | + LandingPageProject entity = new LandingPageProject(); | ||
| 43 | + BeanUtils.copyProperties(this, entity); | ||
| 44 | + return entity; | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 将实体类转换为DTO | ||
| 49 | + */ | ||
| 50 | + public static LandingPageProjectDTO fromEntity(LandingPageProject entity) { | ||
| 51 | + LandingPageProjectDTO dto = new LandingPageProjectDTO(); | ||
| 52 | + BeanUtils.copyProperties(entity, dto); | ||
| 53 | + return dto; | ||
| 54 | + } | ||
| 55 | +} |
| 1 | +package com.aigeo.landingpage.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.landingpage.entity.LandingPageProject; | ||
| 4 | +import com.aigeo.landingpage.repository.LandingPageProjectRepository; | ||
| 5 | +import com.aigeo.landingpage.service.LandingPageProjectService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 落地页项目服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class LandingPageProjectServiceImpl implements LandingPageProjectService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private LandingPageProjectRepository landingPageProjectRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public LandingPageProject save(LandingPageProject project) { | ||
| 23 | + return landingPageProjectRepository.save(project); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<LandingPageProject> findById(Integer id) { | ||
| 28 | + return landingPageProjectRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<LandingPageProject> findByCompanyId(Integer companyId) { | ||
| 33 | + return landingPageProjectRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<LandingPageProject> findByUserId(Integer userId) { | ||
| 38 | + return landingPageProjectRepository.findByUserId(userId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<LandingPageProject> findByStatus(LandingPageProject.ProjectStatus status) { | ||
| 43 | + return landingPageProjectRepository.findByStatus(status); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<LandingPageProject> findByCompanyIdAndStatus(Integer companyId, LandingPageProject.ProjectStatus status) { | ||
| 48 | + return landingPageProjectRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<LandingPageProject> findByUserIdAndStatus(Integer userId, LandingPageProject.ProjectStatus status) { | ||
| 53 | + return landingPageProjectRepository.findByUserIdAndStatus(userId, status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<LandingPageProject> findAll() { | ||
| 58 | + return landingPageProjectRepository.findAll(); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public void deleteById(Integer id) { | ||
| 63 | + landingPageProjectRepository.deleteById(id); | ||
| 64 | + } | ||
| 65 | +} |
| 1 | +package com.aigeo.platform.dto; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 发布平台DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@Schema(description = "发布平台数据传输对象") | ||
| 13 | +public class PublishingPlatformDTO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "平台ID") | ||
| 16 | + private Integer id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "平台类型ID") | ||
| 19 | + private Integer typeId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "平台名称") | ||
| 22 | + private String name; | ||
| 23 | + | ||
| 24 | + @Schema(description = "平台代码") | ||
| 25 | + private String code; | ||
| 26 | + | ||
| 27 | + @Schema(description = "平台描述") | ||
| 28 | + private String description; | ||
| 29 | + | ||
| 30 | + @Schema(description = "平台图标") | ||
| 31 | + private String icon; | ||
| 32 | + | ||
| 33 | + @Schema(description = "认证类型") | ||
| 34 | + private String authType; | ||
| 35 | + | ||
| 36 | + @Schema(description = "API配置模板") | ||
| 37 | + private String apiConfigTemplate; | ||
| 38 | + | ||
| 39 | + @Schema(description = "字符限制") | ||
| 40 | + private Integer characterLimit; | ||
| 41 | + | ||
| 42 | + @Schema(description = "是否启用") | ||
| 43 | + private Boolean isActive; | ||
| 44 | + | ||
| 45 | + @Schema(description = "创建时间") | ||
| 46 | + private LocalDateTime createdAt; | ||
| 47 | + | ||
| 48 | + @Schema(description = "更新时间") | ||
| 49 | + private LocalDateTime updatedAt; | ||
| 50 | +} |
| 1 | +package com.aigeo.platform.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.platform.entity.PublishingPlatform; | ||
| 4 | +import com.aigeo.platform.repository.PublishingPlatformRepository; | ||
| 5 | +import com.aigeo.platform.service.PublishingPlatformService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 发布平台服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class PublishingPlatformServiceImpl implements PublishingPlatformService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private PublishingPlatformRepository publishingPlatformRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public PublishingPlatform save(PublishingPlatform platform) { | ||
| 23 | + return publishingPlatformRepository.save(platform); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<PublishingPlatform> findById(Integer id) { | ||
| 28 | + return publishingPlatformRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<PublishingPlatform> findByTypeId(Integer typeId) { | ||
| 33 | + return publishingPlatformRepository.findByTypeId(typeId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<PublishingPlatform> findActivePlatforms() { | ||
| 38 | + return publishingPlatformRepository.findByIsActiveTrue(); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<PublishingPlatform> findActiveByTypeId(Integer typeId) { | ||
| 43 | + return publishingPlatformRepository.findByTypeIdAndIsActiveTrue(typeId); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public PublishingPlatform findByCode(String code) { | ||
| 48 | + return publishingPlatformRepository.findByCode(code); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<PublishingPlatform> findAll() { | ||
| 53 | + return publishingPlatformRepository.findAll(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void deleteById(Integer id) { | ||
| 58 | + publishingPlatformRepository.deleteById(id); | ||
| 59 | + } | ||
| 60 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface ArticleGenerationTaskRepository extends JpaRepository<ArticleGenerationTask, Integer> { | ||
| 11 | + List<ArticleGenerationTask> findByCompanyId(Integer companyId); | ||
| 12 | + List<ArticleGenerationTask> findByCompanyIdAndStatus(Integer companyId, String status); | ||
| 13 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.company.entity.Company; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.Optional; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface CompanyRepository extends JpaRepository<Company, Integer> { | ||
| 11 | + Optional<Company> findByName(String name); | ||
| 12 | + Optional<Company> findByDomain(String domain); | ||
| 13 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Optional; | ||
| 9 | + | ||
| 10 | +@Repository | ||
| 11 | +public interface DifyApiConfigRepository extends JpaRepository<DifyApiConfig, Integer> { | ||
| 12 | + List<DifyApiConfig> findByCompanyId(Integer companyId); | ||
| 13 | + List<DifyApiConfig> findByCompanyIdAndIsActiveTrue(Integer companyId); | ||
| 14 | + Optional<DifyApiConfig> findByCompanyIdAndIsActiveTrueOrderByCreatedAtDesc(Integer companyId); | ||
| 15 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.ai.entity.Feature; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface FeatureRepository extends JpaRepository<Feature, Integer> { | ||
| 11 | + List<Feature> findByIsActiveTrue(); | ||
| 12 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.article.entity.GeneratedArticle; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface GeneratedArticleRepository extends JpaRepository<GeneratedArticle, Integer> { | ||
| 11 | + List<GeneratedArticle> findByTaskId(Integer taskId); | ||
| 12 | + List<GeneratedArticle> findByCompanyId(Integer companyId); | ||
| 13 | + List<GeneratedArticle> findByCompanyIdAndStatus(Integer companyId, String status); | ||
| 14 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.landingpage.entity.LandingPageProject; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface LandingPageProjectRepository extends JpaRepository<LandingPageProject, Integer> { | ||
| 11 | + List<LandingPageProject> findByCompanyId(Integer companyId); | ||
| 12 | + List<LandingPageProject> findByCompanyIdAndStatus(Integer companyId, String status); | ||
| 13 | +} |
| 1 | +package com.aigeo.repository; | ||
| 2 | + | ||
| 3 | +import com.aigeo.company.entity.User; | ||
| 4 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.Optional; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +public interface UserRepository extends JpaRepository<User, Integer> { | ||
| 11 | + Optional<User> findByUsername(String username); | ||
| 12 | + Optional<User> findByEmail(String email); | ||
| 13 | +} |
src/main/java/com/aigeo/util/JwtUtil.java
0 → 100644
| 1 | +package com.aigeo.util; | ||
| 2 | + | ||
| 3 | +import com.aigeo.company.entity.User; | ||
| 4 | +import io.jsonwebtoken.Claims; | ||
| 5 | +import io.jsonwebtoken.Jwts; | ||
| 6 | +import io.jsonwebtoken.io.Decoders; | ||
| 7 | +import io.jsonwebtoken.security.Keys; | ||
| 8 | +import org.springframework.beans.factory.annotation.Value; | ||
| 9 | +import org.springframework.stereotype.Component; | ||
| 10 | + | ||
| 11 | +import javax.crypto.SecretKey; | ||
| 12 | +import java.util.Date; | ||
| 13 | +import java.util.HashMap; | ||
| 14 | +import java.util.Map; | ||
| 15 | +import java.util.function.Function; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * JWT工具类 | ||
| 19 | + */ | ||
| 20 | +@Component | ||
| 21 | +public class JwtUtil { | ||
| 22 | + | ||
| 23 | + @Value("${jwt.secret:mySecretKeyForAigeoApplicationWhichIsLongEnough}") | ||
| 24 | + private String secret; | ||
| 25 | + | ||
| 26 | + @Value("${jwt.expiration:86400}") | ||
| 27 | + private Long expiration; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 获取签名密钥 | ||
| 31 | + */ | ||
| 32 | + private SecretKey getSigningKey() { | ||
| 33 | + byte[] keyBytes = Decoders.BASE64.decode(secret); | ||
| 34 | + return Keys.hmacShaKeyFor(keyBytes); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 从token中获取用户名 | ||
| 39 | + */ | ||
| 40 | + public String getUsernameFromToken(String token) { | ||
| 41 | + Claims claims = getClaimsFromToken(token); | ||
| 42 | + return claims.getSubject(); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 从token中提取用户名(别名方法) | ||
| 47 | + */ | ||
| 48 | + public String extractUsername(String token) { | ||
| 49 | + return getUsernameFromToken(token); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 从token中获取Claims | ||
| 54 | + */ | ||
| 55 | + private Claims getClaimsFromToken(String token) { | ||
| 56 | + return Jwts.parser() | ||
| 57 | + .verifyWith(getSigningKey()) | ||
| 58 | + .build() | ||
| 59 | + .parseSignedClaims(token) | ||
| 60 | + .getPayload(); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 检查token是否过期 | ||
| 65 | + */ | ||
| 66 | + private boolean isTokenExpired(String token) { | ||
| 67 | + Date expiredDate = getClaimsFromToken(token).getExpiration(); | ||
| 68 | + return expiredDate.before(new Date()); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + public Date extractExpiration(String token) { | ||
| 72 | + return getClaimsFromToken(token).getExpiration(); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { | ||
| 76 | + final Claims claims = getClaimsFromToken(token); | ||
| 77 | + return claimsResolver.apply(claims); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + public String generateToken(User user) { | ||
| 81 | + Map<String, Object> claims = new HashMap<>(); | ||
| 82 | + return createToken(claims, user.getUsername()); | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + private String createToken(Map<String, Object> claims, String subject) { | ||
| 86 | + return Jwts.builder() | ||
| 87 | + .claims(claims) | ||
| 88 | + .subject(subject) | ||
| 89 | + .issuedAt(new Date(System.currentTimeMillis())) | ||
| 90 | + .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) | ||
| 91 | + .signWith(getSigningKey()) | ||
| 92 | + .compact(); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + public Boolean validateToken(String token, User user) { | ||
| 96 | + if (user == null) { | ||
| 97 | + return false; | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + final String username = getUsernameFromToken(token); | ||
| 101 | + return (username.equals(user.getUsername()) && !isTokenExpired(token)); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 获取过期时间(秒) | ||
| 106 | + */ | ||
| 107 | + public Long getExpirationTimeSeconds() { | ||
| 108 | + return expiration; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 获取过期时间(毫秒) | ||
| 113 | + */ | ||
| 114 | + public Long getExpirationTime() { | ||
| 115 | + return expiration * 1000; | ||
| 116 | + } | ||
| 117 | +} |
| 1 | +package com.aigeo.website.dto; | ||
| 2 | + | ||
| 3 | +import com.aigeo.website.entity.WebsiteProject; | ||
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.time.LocalDateTime; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 网站项目DTO | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +@Schema(description = "网站项目数据传输对象") | ||
| 14 | +public class WebsiteProjectDTO { | ||
| 15 | + | ||
| 16 | + @Schema(description = "项目ID") | ||
| 17 | + private Integer id; | ||
| 18 | + | ||
| 19 | + @Schema(description = "公司ID") | ||
| 20 | + private Integer companyId; | ||
| 21 | + | ||
| 22 | + @Schema(description = "用户ID") | ||
| 23 | + private Integer userId; | ||
| 24 | + | ||
| 25 | + @Schema(description = "项目名称") | ||
| 26 | + private String projectName; | ||
| 27 | + | ||
| 28 | + @Schema(description = "网站名称") | ||
| 29 | + private String siteName; | ||
| 30 | + | ||
| 31 | + @Schema(description = "项目状态") | ||
| 32 | + private WebsiteProject.ProjectStatus status; | ||
| 33 | + | ||
| 34 | + @Schema(description = "创建时间") | ||
| 35 | + private LocalDateTime createdAt; | ||
| 36 | + | ||
| 37 | + @Schema(description = "最后更新时间") | ||
| 38 | + private LocalDateTime updatedAt; | ||
| 39 | +} |
| 1 | +package com.aigeo.website.service.impl; | ||
| 2 | + | ||
| 3 | +import com.aigeo.website.entity.WebsiteProject; | ||
| 4 | +import com.aigeo.website.repository.WebsiteProjectRepository; | ||
| 5 | +import com.aigeo.website.service.WebsiteProjectService; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.stereotype.Service; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Optional; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 网站项目服务实现类 | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +public class WebsiteProjectServiceImpl implements WebsiteProjectService { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + private WebsiteProjectRepository websiteProjectRepository; | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public WebsiteProject save(WebsiteProject project) { | ||
| 23 | + return websiteProjectRepository.save(project); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public Optional<WebsiteProject> findById(Integer id) { | ||
| 28 | + return websiteProjectRepository.findById(id); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public List<WebsiteProject> findByCompanyId(Integer companyId) { | ||
| 33 | + return websiteProjectRepository.findByCompanyId(companyId); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public List<WebsiteProject> findByUserId(Integer userId) { | ||
| 38 | + return websiteProjectRepository.findByUserId(userId); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public List<WebsiteProject> findByStatus(WebsiteProject.ProjectStatus status) { | ||
| 43 | + return websiteProjectRepository.findByStatus(status); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public List<WebsiteProject> findByCompanyIdAndStatus(Integer companyId, WebsiteProject.ProjectStatus status) { | ||
| 48 | + return websiteProjectRepository.findByCompanyIdAndStatus(companyId, status); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Override | ||
| 52 | + public List<WebsiteProject> findByUserIdAndStatus(Integer userId, WebsiteProject.ProjectStatus status) { | ||
| 53 | + return websiteProjectRepository.findByUserIdAndStatus(userId, status); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public List<WebsiteProject> findAll() { | ||
| 58 | + return websiteProjectRepository.findAll(); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public void deleteById(Integer id) { | ||
| 63 | + websiteProjectRepository.deleteById(id); | ||
| 64 | + } | ||
| 65 | +} |
src/main/resources/application.yml
0 → 100644
| 1 | +server: | ||
| 2 | + port: 8080 | ||
| 3 | + servlet: | ||
| 4 | + context-path: /api | ||
| 5 | + encoding: | ||
| 6 | + charset: UTF-8 | ||
| 7 | + enabled: true | ||
| 8 | + force: true | ||
| 9 | + | ||
| 10 | +spring: | ||
| 11 | + profiles: | ||
| 12 | + active: dev | ||
| 13 | + application: | ||
| 14 | + name: aigeo | ||
| 15 | + main: | ||
| 16 | + allow-bean-definition-overriding: true | ||
| 17 | + | ||
| 18 | + # 数据库配置 | ||
| 19 | + datasource: | ||
| 20 | + driver-class-name: com.mysql.cj.jdbc.Driver | ||
| 21 | + url: jdbc:mysql://115.175.226.205:33062/ai_3?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true | ||
| 22 | + username: root | ||
| 23 | + password: sz1234567890 | ||
| 24 | + type: com.zaxxer.hikari.HikariDataSource | ||
| 25 | + hikari: | ||
| 26 | + minimum-idle: 5 | ||
| 27 | + maximum-pool-size: 20 | ||
| 28 | + auto-commit: true | ||
| 29 | + idle-timeout: 30000 | ||
| 30 | + pool-name: HikariCP | ||
| 31 | + max-lifetime: 1800000 | ||
| 32 | + connection-timeout: 30000 | ||
| 33 | + connection-test-query: SELECT 1 | ||
| 34 | + | ||
| 35 | + # JPA配置 | ||
| 36 | + jpa: | ||
| 37 | + hibernate: | ||
| 38 | + ddl-auto: none | ||
| 39 | + show-sql: false | ||
| 40 | + properties: | ||
| 41 | + hibernate: | ||
| 42 | + dialect: org.hibernate.dialect.MySQL8Dialect | ||
| 43 | + format_sql: true | ||
| 44 | + use_sql_comments: false | ||
| 45 | + open-in-view: false | ||
| 46 | + | ||
| 47 | + # Redis配置 | ||
| 48 | + data: | ||
| 49 | + redis: | ||
| 50 | + host: 115.175.226.205 | ||
| 51 | + port: 6379 | ||
| 52 | + database: 0 | ||
| 53 | + password: sz123321 | ||
| 54 | + timeout: 3000 | ||
| 55 | + jedis: | ||
| 56 | + pool: | ||
| 57 | + max-active: 20 | ||
| 58 | + max-wait: -1 | ||
| 59 | + max-idle: 10 | ||
| 60 | + min-idle: 0 | ||
| 61 | + | ||
| 62 | + # JSON配置 | ||
| 63 | + jackson: | ||
| 64 | + time-zone: GMT+8 | ||
| 65 | + date-format: yyyy-MM-dd HH:mm:ss | ||
| 66 | + default-property-inclusion: NON_NULL | ||
| 67 | + serialization: | ||
| 68 | + write-dates-as-timestamps: false | ||
| 69 | + write-date-timestamps-as-nanoseconds: false | ||
| 70 | + deserialization: | ||
| 71 | + read-date-timestamps-as-nanoseconds: false | ||
| 72 | + | ||
| 73 | + # 文件上传配置 | ||
| 74 | + servlet: | ||
| 75 | + multipart: | ||
| 76 | + max-file-size: 100MB | ||
| 77 | + max-request-size: 100MB | ||
| 78 | + enabled: true | ||
| 79 | + | ||
| 80 | + # 定时任务配置 | ||
| 81 | + quartz: | ||
| 82 | + job-store-type: jdbc | ||
| 83 | + jdbc: | ||
| 84 | + initialize-schema: never | ||
| 85 | + properties: | ||
| 86 | + org: | ||
| 87 | + quartz: | ||
| 88 | + scheduler: | ||
| 89 | + instanceName: AigeoScheduler | ||
| 90 | + instanceId: AUTO | ||
| 91 | + jobStore: | ||
| 92 | + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore | ||
| 93 | + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate | ||
| 94 | + tablePrefix: QRTZ_ | ||
| 95 | + isClustered: false | ||
| 96 | + useProperties: false | ||
| 97 | + threadPool: | ||
| 98 | + class: org.quartz.simpl.SimpleThreadPool | ||
| 99 | + threadCount: 10 | ||
| 100 | + threadPriority: 5 | ||
| 101 | + threadsInheritContextClassLoaderOfInitializingThread: true | ||
| 102 | + | ||
| 103 | + # Security配置 | ||
| 104 | + security: | ||
| 105 | + user: | ||
| 106 | + name: admin | ||
| 107 | + password: admin123 | ||
| 108 | + sql: | ||
| 109 | + init: | ||
| 110 | + data-locations: | ||
| 111 | + mode: | ||
| 112 | + schema-locations: | ||
| 113 | + | ||
| 114 | +# MyBatis Plus配置 | ||
| 115 | +mybatis-plus: | ||
| 116 | + configuration: | ||
| 117 | + map-underscore-to-camel-case: true | ||
| 118 | + cache-enabled: false | ||
| 119 | + call-setters-on-nulls: true | ||
| 120 | + jdbc-type-for-null: 'null' | ||
| 121 | + | ||
| 122 | +# SpringDoc配置 | ||
| 123 | +springdoc: | ||
| 124 | + swagger-ui: | ||
| 125 | + path: /swagger-ui.html | ||
| 126 | + tags-sorter: alpha | ||
| 127 | + operations-sorter: alpha | ||
| 128 | + api-docs: | ||
| 129 | + path: /v3/api-docs | ||
| 130 | + group-configs: | ||
| 131 | + - group: 'default' | ||
| 132 | + paths-to-match: '/**' | ||
| 133 | + packages-to-scan: com.aigeo | ||
| 134 | + | ||
| 135 | +# Knife4j配置 | ||
| 136 | +knife4j: | ||
| 137 | + enable: true | ||
| 138 | + production: false | ||
| 139 | + basic: | ||
| 140 | + enable: false | ||
| 141 | + setting: | ||
| 142 | + language: zh_cn |
src/main/resources/data.sql
0 → 100644
| 1 | +-- 初始化数据脚本 | ||
| 2 | + | ||
| 3 | +-- 插入默认公司 | ||
| 4 | +INSERT INTO ai_companies (name, status, created_at, updated_at) | ||
| 5 | +VALUES ('Default Company', 'active', NOW(), NOW()); | ||
| 6 | + | ||
| 7 | +-- 插入默认用户 (密码为 "password" 的BCrypt哈希) | ||
| 8 | +INSERT INTO ai_users (company_id, username, email, password_hash, role, is_active, created_at, updated_at) | ||
| 9 | +VALUES (1, 'admin', 'admin@aigeo.com', '$2a$10$wQ8vI6jJ2x6Dit4G3E0jVOvH9JqKz3Zs5r1D4r6H7a8B9c0D1e2F3g4', 'admin', 1, NOW(), NOW()); | ||
| 10 | + | ||
| 11 | +-- 插入AI功能模块 | ||
| 12 | +INSERT INTO ai_features (feature_key, name, description, category, is_premium, sort_order, is_active, created_at) | ||
| 13 | +VALUES | ||
| 14 | +('ai_article', 'AI文章生成', '基于关键词和主题自动生成高质量文章', 'content', 0, 1, 1, NOW()), | ||
| 15 | +('ai_landing_page', 'AI落地页生成', '根据业务需求自动生成营销落地页', 'marketing', 0, 2, 1, NOW()), | ||
| 16 | +('ai_website', 'AI网站生成', '一键生成企业官网或电商网站', 'website', 1, 3, 1, NOW()); | ||
| 17 | + | ||
| 18 | +-- 插入文章类型 | ||
| 19 | +INSERT INTO ai_article_types (name, description, is_active, created_at, updated_at) | ||
| 20 | +VALUES | ||
| 21 | +('产品介绍', '产品介绍类文章', 1, NOW(), NOW()), | ||
| 22 | +('新闻稿', '企业新闻稿', 1, NOW(), NOW()), | ||
| 23 | +('博客文章', '技术博客或行业分析文章', 1, NOW(), NOW()); | ||
| 24 | + | ||
| 25 | +-- 插入落地页模板 | ||
| 26 | +INSERT INTO ai_landing_page_templates (name, code, description, is_active, created_at, updated_at) | ||
| 27 | +VALUES | ||
| 28 | +('单栏布局', 'single-column', '简洁的单栏布局模板', 1, NOW(), NOW()), | ||
| 29 | +('两栏布局', 'two-column', '经典的两栏布局模板', 1, NOW(), NOW()), | ||
| 30 | +('产品展示', 'product-showcase', '专注于产品展示的模板', 1, NOW(), NOW()); |
-
请 注册 或 登录 后发表评论