正在显示
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()); |
-
请 注册 或 登录 后发表评论