作者 徐宝林

初始化项目0908

正在显示 87 个修改的文件 包含 4410 行增加0 行删除

要显示太多修改。

为保证性能只显示 87 of 87+ 个文件。

  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 +本项目仅供学习和参考使用。
此 diff 太大无法显示。
  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 +/**
  2 + * AI模块控制器包
  3 + */
  4 +package com.aigeo.ai.controller;
  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 +/**
  2 + * AI模块实体包
  3 + */
  4 +package com.aigeo.ai.entity;
  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 +/**
  2 + * AI模块数据访问包
  3 + */
  4 +package com.aigeo.ai.repository;
  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 +/**
  2 + * AI模块业务逻辑包
  3 + */
  4 +package com.aigeo.ai.service;
  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 +/**
  2 + * 文章模块控制器包
  3 + */
  4 +package com.aigeo.article.controller;
  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 +/**
  2 + * 文章模块实体包
  3 + */
  4 +package com.aigeo.article.entity;
  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 +/**
  2 + * 文章模块数据访问包
  3 + */
  4 +package com.aigeo.article.repository;
  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 +/**
  2 + * 文章模块业务逻辑包
  3 + */
  4 +package com.aigeo.article.service;
  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 +}
  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 +/**
  2 + * 公司模块控制器包
  3 + */
  4 +package com.aigeo.company.controller;
  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 +/**
  2 + * 公司模块实体包
  3 + */
  4 +package com.aigeo.company.entity;
  1 +/**
  2 + * 公司模块数据访问包
  3 + */
  4 +package com.aigeo.company.repository;
  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 +/**
  2 + * 公司模块业务逻辑包
  3 + */
  4 +package com.aigeo.company.service;
  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.Configuration;
  4 +import org.springframework.scheduling.annotation.EnableScheduling;
  5 +
  6 +@Configuration
  7 +@EnableScheduling
  8 +public class QuartzConfig {
  9 +
  10 +}
  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 +/**
  2 + * 关键词模块控制器包
  3 + */
  4 +package com.aigeo.keyword.controller;
  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 +/**
  2 + * 关键词模块数据传输对象包
  3 + */
  4 +package com.aigeo.keyword.dto;
  1 +/**
  2 + * 关键词模块实体包
  3 + */
  4 +package com.aigeo.keyword.entity;
  1 +/**
  2 + * 关键词模块数据访问包
  3 + */
  4 +package com.aigeo.keyword.repository;
  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 +/**
  2 + * 关键词模块业务逻辑包
  3 + */
  4 +package com.aigeo.keyword.service;
  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 +/**
  2 + * 落地页模块控制器包
  3 + */
  4 +package com.aigeo.landingpage.controller;
  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 +/**
  2 + * 落地页模块实体包
  3 + */
  4 +package com.aigeo.landingpage.entity;
  1 +/**
  2 + * 落地页模块数据访问包
  3 + */
  4 +package com.aigeo.landingpage.repository;
  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 +/**
  2 + * 落地页模块业务逻辑包
  3 + */
  4 +package com.aigeo.landingpage.service;
  1 +/**
  2 + * 平台模块控制器包
  3 + */
  4 +package com.aigeo.platform.controller;
  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 +/**
  2 + * 平台模块数据传输对象包
  3 + */
  4 +package com.aigeo.platform.dto;
  1 +/**
  2 + * 平台模块实体包
  3 + */
  4 +package com.aigeo.platform.entity;
  1 +/**
  2 + * 平台模块数据访问包
  3 + */
  4 +package com.aigeo.platform.repository;
  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 +/**
  2 + * 平台模块业务逻辑包
  3 + */
  4 +package com.aigeo.platform.service;
  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 +}
  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 +/**
  2 + * 网站模块控制器包
  3 + */
  4 +package com.aigeo.website.controller;
  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 +/**
  2 + * 网站模块数据传输对象包
  3 + */
  4 +package com.aigeo.website.dto;
  1 +/**
  2 + * 网站模块实体包
  3 + */
  4 +package com.aigeo.website.entity;
  1 +/**
  2 + * 网站模块数据访问包
  3 + */
  4 +package com.aigeo.website.repository;
  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 +}
  1 +/**
  2 + * 网站模块业务逻辑包
  3 + */
  4 +package com.aigeo.website.service;
  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
  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());