1844 lines
56 KiB
Markdown
1844 lines
56 KiB
Markdown
|
|
# EasyCode Front Workbench Implementation Plan
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** Build an independent EasyCode front workbench where front users log in, generate editable MySQL table drafts through a backend DeepSeek service, preview backend/frontend/admin project files, and download generated source.
|
|||
|
|
|
|||
|
|
**Architecture:** Add a standalone `easycode-web` Vue 3 + Vite app for public/front-user flows. Add `/front/**` APIs to `ruoyi-admin`, store front-user drafts in new `front_*` tables under `ruoyi-generator`, call DeepSeek only through backend `AiGenerateService`, and reuse the existing generator rendering logic for preview/download.
|
|||
|
|
|
|||
|
|
**Tech Stack:** Spring Boot 2.5, Spring Security, MyBatis XML, Fastjson2, Velocity generator, Vue 3, Vite, Vue Router, Axios, Element Plus, Sass.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Scope Check
|
|||
|
|
|
|||
|
|
This is a cross-cutting feature, but the pieces are sequential parts of one product workflow:
|
|||
|
|
|
|||
|
|
1. Database/auth foundation.
|
|||
|
|
2. Project draft and editable database design.
|
|||
|
|
3. DeepSeek-backed database generation.
|
|||
|
|
4. Generator preview/download adaptation.
|
|||
|
|
5. Independent front app and end-to-end integration.
|
|||
|
|
|
|||
|
|
Implement in the task order below. Each task should end with tests and a commit.
|
|||
|
|
|
|||
|
|
## File Map
|
|||
|
|
|
|||
|
|
### SQL
|
|||
|
|
|
|||
|
|
- Create `sql/front_workbench.sql`: standalone DDL for new front-user and draft tables.
|
|||
|
|
- Modify `sql/db.sql`: include the same DDL so a fresh database has the front workbench tables.
|
|||
|
|
|
|||
|
|
### Common and Framework
|
|||
|
|
|
|||
|
|
- Modify `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java`: add login type constants.
|
|||
|
|
- Modify `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java`: add `loginType`.
|
|||
|
|
- Modify `ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java`: prevent front tokens from authenticating non-`/front/**` requests, and prevent admin tokens from authenticating protected `/front/**` requests.
|
|||
|
|
- Modify `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java`: allow anonymous `/front/auth/login` and `/front/auth/register`.
|
|||
|
|
- Modify `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java`: mark admin logins as admin tokens.
|
|||
|
|
|
|||
|
|
### Generator Module
|
|||
|
|
|
|||
|
|
- Create domain classes under `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/`:
|
|||
|
|
- `FrontUser.java`
|
|||
|
|
- `FrontProject.java`
|
|||
|
|
- `FrontProjectTable.java`
|
|||
|
|
- `FrontProjectColumn.java`
|
|||
|
|
- `FrontProjectGeneration.java`
|
|||
|
|
- `FrontProjectFile.java`
|
|||
|
|
- Create DTO classes under `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/`:
|
|||
|
|
- `FrontLoginBody.java`
|
|||
|
|
- `FrontRegisterBody.java`
|
|||
|
|
- `FrontProjectCreateRequest.java`
|
|||
|
|
- `FrontProjectUpdateRequest.java`
|
|||
|
|
- `GenerateDatabaseRequest.java`
|
|||
|
|
- `DatabaseDesignResponse.java`
|
|||
|
|
- `DatabaseTableDesign.java`
|
|||
|
|
- `DatabaseColumnDesign.java`
|
|||
|
|
- `PreviewFileRequest.java`
|
|||
|
|
- `ProjectStructureResponse.java`
|
|||
|
|
- Create mappers under `ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/`:
|
|||
|
|
- `FrontUserMapper.java`
|
|||
|
|
- `FrontProjectMapper.java`
|
|||
|
|
- `FrontProjectTableMapper.java`
|
|||
|
|
- `FrontProjectColumnMapper.java`
|
|||
|
|
- `FrontProjectGenerationMapper.java`
|
|||
|
|
- `FrontProjectFileMapper.java`
|
|||
|
|
- Create mapper XML under `ruoyi-generator/src/main/resources/mapper/front/` with matching names.
|
|||
|
|
- Create services under `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/`:
|
|||
|
|
- `IFrontUserService.java`
|
|||
|
|
- `FrontUserServiceImpl.java`
|
|||
|
|
- `IFrontProjectService.java`
|
|||
|
|
- `FrontProjectServiceImpl.java`
|
|||
|
|
- `IAiGenerateService.java`
|
|||
|
|
- `AiGenerateServiceImpl.java`
|
|||
|
|
- `IDeepSeekClient.java`
|
|||
|
|
- `HttpDeepSeekClient.java`
|
|||
|
|
- `AiDatabaseSchemaValidator.java`
|
|||
|
|
- `MysqlDdlBuilder.java`
|
|||
|
|
- `FrontProjectConverter.java`
|
|||
|
|
- `IFrontProjectPreviewService.java`
|
|||
|
|
- `FrontProjectPreviewServiceImpl.java`
|
|||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/config/`:
|
|||
|
|
- Create `DeepSeekProperties.java`.
|
|||
|
|
- Modify existing generator rendering:
|
|||
|
|
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenProjectService.java`
|
|||
|
|
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java`
|
|||
|
|
Add overloads that accept a transient `GenProject` with draft tables.
|
|||
|
|
|
|||
|
|
### Admin Controllers
|
|||
|
|
|
|||
|
|
- Create `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontAuthController.java`.
|
|||
|
|
- Create `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java`.
|
|||
|
|
- Create `ruoyi-admin/src/main/java/com/ruoyi/web/service/front/FrontAuthService.java`.
|
|||
|
|
|
|||
|
|
### Front App
|
|||
|
|
|
|||
|
|
- Create project root `easycode-web/`.
|
|||
|
|
- Create `easycode-web/package.json`, `vite.config.js`, `index.html`, `.env.development`, `.env.production`.
|
|||
|
|
- Create app files:
|
|||
|
|
- `easycode-web/src/main.js`
|
|||
|
|
- `easycode-web/src/App.vue`
|
|||
|
|
- `easycode-web/src/router/index.js`
|
|||
|
|
- `easycode-web/src/api/request.js`
|
|||
|
|
- `easycode-web/src/api/auth.js`
|
|||
|
|
- `easycode-web/src/api/project.js`
|
|||
|
|
- `easycode-web/src/utils/auth.js`
|
|||
|
|
- `easycode-web/src/styles/global.scss`
|
|||
|
|
- `easycode-web/src/components/AppHeader.vue`
|
|||
|
|
- `easycode-web/src/components/DatabaseDesigner.vue`
|
|||
|
|
- `easycode-web/src/components/ProjectStructureTree.vue`
|
|||
|
|
- `easycode-web/src/components/CodePreview.vue`
|
|||
|
|
- `easycode-web/src/views/HomeView.vue`
|
|||
|
|
- `easycode-web/src/views/LoginView.vue`
|
|||
|
|
- `easycode-web/src/views/RegisterView.vue`
|
|||
|
|
- `easycode-web/src/views/GenerateView.vue`
|
|||
|
|
- `easycode-web/src/views/PreviewView.vue`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 1: Add Front Workbench Database Tables
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `sql/front_workbench.sql`
|
|||
|
|
- Modify: `sql/db.sql`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Write the DDL script**
|
|||
|
|
|
|||
|
|
Create `sql/front_workbench.sql` with this schema. Use `utf8mb4`, `innodb`, and `front_` prefixes.
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- =================================================
|
|||
|
|
-- EasyCode front workbench tables
|
|||
|
|
-- Import after sql/db.sql, or use the copy appended to sql/db.sql
|
|||
|
|
-- =================================================
|
|||
|
|
|
|||
|
|
drop table if exists front_project_file;
|
|||
|
|
drop table if exists front_project_generation;
|
|||
|
|
drop table if exists front_project_column;
|
|||
|
|
drop table if exists front_project_table;
|
|||
|
|
drop table if exists front_project;
|
|||
|
|
drop table if exists front_user;
|
|||
|
|
|
|||
|
|
create table front_user (
|
|||
|
|
user_id bigint(20) not null auto_increment comment '前台用户ID',
|
|||
|
|
username varchar(50) not null comment '用户名',
|
|||
|
|
password varchar(100) not null comment '密码',
|
|||
|
|
nickname varchar(50) default '' comment '昵称',
|
|||
|
|
email varchar(100) default '' comment '邮箱',
|
|||
|
|
phone varchar(20) default '' comment '手机号',
|
|||
|
|
status char(1) default '0' comment '状态(0正常 1停用)',
|
|||
|
|
del_flag char(1) default '0' comment '删除标志(0存在 2删除)',
|
|||
|
|
last_login_time datetime default null comment '最后登录时间',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
update_time datetime default null comment '更新时间',
|
|||
|
|
remark varchar(500) default null comment '备注',
|
|||
|
|
primary key (user_id),
|
|||
|
|
unique key uk_front_user_username (username),
|
|||
|
|
key idx_front_user_email (email),
|
|||
|
|
key idx_front_user_phone (phone)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台用户表';
|
|||
|
|
|
|||
|
|
create table front_project (
|
|||
|
|
project_id bigint(20) not null auto_increment comment '前台项目ID',
|
|||
|
|
user_id bigint(20) not null comment '前台用户ID',
|
|||
|
|
project_name varchar(100) not null comment '项目名称',
|
|||
|
|
project_file_name varchar(100) default '' comment '项目文件名',
|
|||
|
|
package_name varchar(100) default 'com.easycode.generated' comment '包名',
|
|||
|
|
version varchar(20) default '1.0.0' comment '版本号',
|
|||
|
|
project_desc varchar(1000) default '' comment '项目描述',
|
|||
|
|
industry_template varchar(50) default '' comment '行业模板',
|
|||
|
|
front_framework varchar(50) default 'Vue3 + Element Plus' comment '前台框架',
|
|||
|
|
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
|
|||
|
|
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
|||
|
|
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
|||
|
|
status char(1) default '0' comment '状态(0正常 1停用)',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
update_time datetime default null comment '更新时间',
|
|||
|
|
remark varchar(500) default null comment '备注',
|
|||
|
|
primary key (project_id),
|
|||
|
|
key idx_front_project_user_id (user_id),
|
|||
|
|
constraint fk_front_project_user foreign key (user_id) references front_user(user_id)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台项目草稿表';
|
|||
|
|
|
|||
|
|
create table front_project_table (
|
|||
|
|
table_id bigint(20) not null auto_increment comment '表设计ID',
|
|||
|
|
project_id bigint(20) not null comment '项目ID',
|
|||
|
|
user_id bigint(20) not null comment '前台用户ID',
|
|||
|
|
table_name varchar(200) not null comment '表名称',
|
|||
|
|
table_comment varchar(500) default '' comment '表描述',
|
|||
|
|
class_name varchar(100) default '' comment '实体类名称',
|
|||
|
|
module_name varchar(30) default '' comment '生成模块名',
|
|||
|
|
business_name varchar(30) default '' comment '生成业务名',
|
|||
|
|
function_name varchar(50) default '' comment '生成功能名',
|
|||
|
|
tpl_category varchar(200) default 'crud' comment '模板类型',
|
|||
|
|
tpl_web_type varchar(30) default 'element-plus' comment '前端模板类型',
|
|||
|
|
options varchar(1000) default '' comment '生成选项',
|
|||
|
|
create_table_sql longtext comment '建表SQL',
|
|||
|
|
sort int default 0 comment '排序',
|
|||
|
|
status char(1) default '0' comment '状态(0正常 1停用)',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
update_time datetime default null comment '更新时间',
|
|||
|
|
primary key (table_id),
|
|||
|
|
unique key uk_front_project_table (project_id, table_name),
|
|||
|
|
key idx_front_project_table_project_id (project_id),
|
|||
|
|
constraint fk_front_project_table_project foreign key (project_id) references front_project(project_id)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台项目表设计';
|
|||
|
|
|
|||
|
|
create table front_project_column (
|
|||
|
|
column_id bigint(20) not null auto_increment comment '字段设计ID',
|
|||
|
|
table_id bigint(20) not null comment '表设计ID',
|
|||
|
|
project_id bigint(20) not null comment '项目ID',
|
|||
|
|
column_name varchar(200) not null comment '字段名称',
|
|||
|
|
column_comment varchar(500) default '' comment '字段描述',
|
|||
|
|
column_type varchar(100) not null comment '字段类型',
|
|||
|
|
java_type varchar(500) default 'String' comment 'Java类型',
|
|||
|
|
java_field varchar(200) default '' comment 'Java字段名',
|
|||
|
|
is_pk char(1) default '0' comment '是否主键(1是)',
|
|||
|
|
is_increment char(1) default '0' comment '是否自增(1是)',
|
|||
|
|
is_required char(1) default '0' comment '是否必填(1是)',
|
|||
|
|
is_insert char(1) default '1' comment '是否插入字段(1是)',
|
|||
|
|
is_edit char(1) default '1' comment '是否编辑字段(1是)',
|
|||
|
|
is_list char(1) default '1' comment '是否列表字段(1是)',
|
|||
|
|
is_query char(1) default '0' comment '是否查询字段(1是)',
|
|||
|
|
query_type varchar(200) default 'EQ' comment '查询方式',
|
|||
|
|
html_type varchar(200) default 'input' comment '显示类型',
|
|||
|
|
dict_type varchar(200) default '' comment '字典类型',
|
|||
|
|
default_value varchar(200) default null comment '默认值',
|
|||
|
|
sort int default 0 comment '排序',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
update_time datetime default null comment '更新时间',
|
|||
|
|
primary key (column_id),
|
|||
|
|
key idx_front_project_column_table_id (table_id),
|
|||
|
|
key idx_front_project_column_project_id (project_id),
|
|||
|
|
constraint fk_front_project_column_table foreign key (table_id) references front_project_table(table_id)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台项目字段设计';
|
|||
|
|
|
|||
|
|
create table front_project_generation (
|
|||
|
|
generation_id bigint(20) not null auto_increment comment '生成记录ID',
|
|||
|
|
project_id bigint(20) not null comment '项目ID',
|
|||
|
|
user_id bigint(20) not null comment '前台用户ID',
|
|||
|
|
generate_type varchar(50) not null comment '生成类型',
|
|||
|
|
provider varchar(50) default 'deepseek' comment 'AI提供方',
|
|||
|
|
prompt_summary varchar(500) default '' comment 'Prompt摘要',
|
|||
|
|
request_payload longtext comment '请求内容',
|
|||
|
|
response_payload longtext comment '响应内容',
|
|||
|
|
success char(1) default '0' comment '是否成功(1成功 0失败)',
|
|||
|
|
error_message varchar(1000) default '' comment '错误信息',
|
|||
|
|
token_usage int default 0 comment 'Token用量',
|
|||
|
|
elapsed_ms bigint(20) default 0 comment '耗时毫秒',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
primary key (generation_id),
|
|||
|
|
key idx_front_project_generation_project_id (project_id)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台项目生成记录';
|
|||
|
|
|
|||
|
|
create table front_project_file (
|
|||
|
|
file_id bigint(20) not null auto_increment comment '文件缓存ID',
|
|||
|
|
project_id bigint(20) not null comment '项目ID',
|
|||
|
|
template_type varchar(50) not null comment '模板类型',
|
|||
|
|
node_id varchar(100) default '' comment '结构节点ID',
|
|||
|
|
table_id bigint(20) default null comment '表设计ID',
|
|||
|
|
file_name varchar(255) not null comment '文件名',
|
|||
|
|
file_path varchar(500) default '' comment '文件路径',
|
|||
|
|
file_content longtext comment '文件内容',
|
|||
|
|
content_hash varchar(64) default '' comment '内容哈希',
|
|||
|
|
create_time datetime default null comment '创建时间',
|
|||
|
|
update_time datetime default null comment '更新时间',
|
|||
|
|
primary key (file_id),
|
|||
|
|
key idx_front_project_file_project_id (project_id),
|
|||
|
|
key idx_front_project_file_lookup (project_id, template_type, table_id)
|
|||
|
|
) engine=innodb auto_increment=100 comment='前台项目预览文件缓存';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Append the same DDL to `sql/db.sql`**
|
|||
|
|
|
|||
|
|
Place it after the existing generator extension tables and before `SET FOREIGN_KEY_CHECKS = 1;`. Keep `drop table if exists` order from child tables to parent tables.
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Run SQL static checks**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git diff --check -- sql/db.sql sql/front_workbench.sql
|
|||
|
|
rg -n "create table front_user|create table front_project|front_project_generation" sql/db.sql sql/front_workbench.sql
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
git diff --check has no output
|
|||
|
|
rg prints all six new front_* table declarations
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add sql/db.sql sql/front_workbench.sql
|
|||
|
|
git commit -m "Add front workbench database schema"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 2: Add Front Token Isolation
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java`
|
|||
|
|
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java`
|
|||
|
|
- Modify: `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java`
|
|||
|
|
- Modify: `ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java`
|
|||
|
|
- Modify: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add login type constants**
|
|||
|
|
|
|||
|
|
In `Constants.java`, add:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/** 后台管理端登录类型 */
|
|||
|
|
public static final String LOGIN_TYPE_ADMIN = "admin";
|
|||
|
|
|
|||
|
|
/** 前台用户登录类型 */
|
|||
|
|
public static final String LOGIN_TYPE_FRONT = "front";
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Add `loginType` to `LoginUser`**
|
|||
|
|
|
|||
|
|
Add field, getter, and setter:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/** 登录类型:admin 后台用户,front 前台用户 */
|
|||
|
|
private String loginType;
|
|||
|
|
|
|||
|
|
public String getLoginType()
|
|||
|
|
{
|
|||
|
|
return loginType;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void setLoginType(String loginType)
|
|||
|
|
{
|
|||
|
|
this.loginType = loginType;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Keep existing constructors unchanged. Existing tokens with `loginType == null` must be treated as admin-compatible.
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Mark existing admin logins**
|
|||
|
|
|
|||
|
|
In `SysLoginService.login`, immediately after:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
add:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
loginUser.setLoginType(Constants.LOGIN_TYPE_ADMIN);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Gate front/admin tokens in JWT filter**
|
|||
|
|
|
|||
|
|
In `JwtAuthenticationTokenFilter`, add helper methods:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private boolean isFrontRequest(HttpServletRequest request)
|
|||
|
|
{
|
|||
|
|
return request.getRequestURI() != null && request.getRequestURI().startsWith("/front/");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private boolean isTokenAllowedForRequest(LoginUser loginUser, HttpServletRequest request)
|
|||
|
|
{
|
|||
|
|
String loginType = loginUser.getLoginType();
|
|||
|
|
boolean frontRequest = isFrontRequest(request);
|
|||
|
|
if (frontRequest)
|
|||
|
|
{
|
|||
|
|
return com.ruoyi.common.constant.Constants.LOGIN_TYPE_FRONT.equals(loginType);
|
|||
|
|
}
|
|||
|
|
return loginType == null || com.ruoyi.common.constant.Constants.LOGIN_TYPE_ADMIN.equals(loginType);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Then change the existing authentication block to:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
if (StringUtils.isNotNull(loginUser)
|
|||
|
|
&& isTokenAllowedForRequest(loginUser, request)
|
|||
|
|
&& StringUtils.isNull(SecurityUtils.getAuthentication()))
|
|||
|
|
{
|
|||
|
|
tokenService.verifyToken(loginUser);
|
|||
|
|
UsernamePasswordAuthenticationToken authenticationToken =
|
|||
|
|
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
|||
|
|
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|||
|
|
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Permit front login/register anonymously**
|
|||
|
|
|
|||
|
|
In `SecurityConfig.filterChain`, extend the anonymous matcher:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
requests.antMatchers("/login", "/register", "/captchaImage",
|
|||
|
|
"/front/auth/login", "/front/auth/register").permitAll()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Do not permit all `/front/**`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Run backend compile**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-admin -am -DskipTests package
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
[INFO] BUILD SUCCESS
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java `
|
|||
|
|
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java `
|
|||
|
|
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java `
|
|||
|
|
ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java `
|
|||
|
|
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
|
|||
|
|
git commit -m "Separate front and admin login tokens"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 3: Add Front Domain, Mappers, and Basic Services
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: front domain classes, mapper interfaces, mapper XML, and basic service interfaces/impls listed in File Map.
|
|||
|
|
- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectConverterTest.java`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Create front domain classes**
|
|||
|
|
|
|||
|
|
Each domain class extends `BaseEntity` where timestamps and remark are needed. Use JavaBean getters/setters, not Lombok.
|
|||
|
|
|
|||
|
|
`FrontUser` fields:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private Long userId;
|
|||
|
|
private String username;
|
|||
|
|
private String password;
|
|||
|
|
private String nickname;
|
|||
|
|
private String email;
|
|||
|
|
private String phone;
|
|||
|
|
private String status;
|
|||
|
|
private String delFlag;
|
|||
|
|
private Date lastLoginTime;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`FrontProject` fields:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private Long projectId;
|
|||
|
|
private Long userId;
|
|||
|
|
private String projectName;
|
|||
|
|
private String projectFileName;
|
|||
|
|
private String packageName;
|
|||
|
|
private String version;
|
|||
|
|
private String projectDesc;
|
|||
|
|
private String industryTemplate;
|
|||
|
|
private String frontFramework;
|
|||
|
|
private String backFramework;
|
|||
|
|
private String generateStatus;
|
|||
|
|
private String previewStatus;
|
|||
|
|
private String status;
|
|||
|
|
private List<FrontProjectTable> tables;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`FrontProjectTable` fields:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private Long tableId;
|
|||
|
|
private Long projectId;
|
|||
|
|
private Long userId;
|
|||
|
|
private String tableName;
|
|||
|
|
private String tableComment;
|
|||
|
|
private String className;
|
|||
|
|
private String moduleName;
|
|||
|
|
private String businessName;
|
|||
|
|
private String functionName;
|
|||
|
|
private String tplCategory;
|
|||
|
|
private String tplWebType;
|
|||
|
|
private String options;
|
|||
|
|
private String createTableSql;
|
|||
|
|
private Integer sort;
|
|||
|
|
private String status;
|
|||
|
|
private List<FrontProjectColumn> columns;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`FrontProjectColumn` fields:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private Long columnId;
|
|||
|
|
private Long tableId;
|
|||
|
|
private Long projectId;
|
|||
|
|
private String columnName;
|
|||
|
|
private String columnComment;
|
|||
|
|
private String columnType;
|
|||
|
|
private String javaType;
|
|||
|
|
private String javaField;
|
|||
|
|
private String isPk;
|
|||
|
|
private String isIncrement;
|
|||
|
|
private String isRequired;
|
|||
|
|
private String isInsert;
|
|||
|
|
private String isEdit;
|
|||
|
|
private String isList;
|
|||
|
|
private String isQuery;
|
|||
|
|
private String queryType;
|
|||
|
|
private String htmlType;
|
|||
|
|
private String dictType;
|
|||
|
|
private String defaultValue;
|
|||
|
|
private Integer sort;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`FrontProjectGeneration` and `FrontProjectFile` mirror the SQL columns.
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Create mapper interfaces**
|
|||
|
|
|
|||
|
|
Use this shape for every mapper:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public interface FrontProjectMapper
|
|||
|
|
{
|
|||
|
|
FrontProject selectFrontProjectById(Long projectId);
|
|||
|
|
|
|||
|
|
List<FrontProject> selectFrontProjectList(FrontProject frontProject);
|
|||
|
|
|
|||
|
|
int insertFrontProject(FrontProject frontProject);
|
|||
|
|
|
|||
|
|
int updateFrontProject(FrontProject frontProject);
|
|||
|
|
|
|||
|
|
int deleteFrontProjectById(Long projectId);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Add ownership-specific methods where needed:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
FrontProject selectFrontProjectByUserAndId(@Param("userId") Long userId, @Param("projectId") Long projectId);
|
|||
|
|
int deleteFrontProjectByUserAndId(@Param("userId") Long userId, @Param("projectId") Long projectId);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
For table and column mappers add:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
List<FrontProjectTable> selectTablesByProjectId(Long projectId);
|
|||
|
|
int deleteTablesByProjectId(Long projectId);
|
|||
|
|
List<FrontProjectColumn> selectColumnsByTableId(Long tableId);
|
|||
|
|
int deleteColumnsByProjectId(Long projectId);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Create mapper XML**
|
|||
|
|
|
|||
|
|
Follow the existing XML style in `mapper/generator/*.xml`. Every XML must contain:
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<resultMap type="FrontProject" id="FrontProjectResult">
|
|||
|
|
<id property="projectId" column="project_id" />
|
|||
|
|
<result property="userId" column="user_id" />
|
|||
|
|
<result property="projectName" column="project_name" />
|
|||
|
|
<result property="projectFileName" column="project_file_name" />
|
|||
|
|
<result property="packageName" column="package_name" />
|
|||
|
|
<result property="version" column="version" />
|
|||
|
|
<result property="projectDesc" column="project_desc" />
|
|||
|
|
<result property="industryTemplate" column="industry_template" />
|
|||
|
|
<result property="frontFramework" column="front_framework" />
|
|||
|
|
<result property="backFramework" column="back_framework" />
|
|||
|
|
<result property="generateStatus" column="generate_status" />
|
|||
|
|
<result property="previewStatus" column="preview_status" />
|
|||
|
|
<result property="status" column="status" />
|
|||
|
|
<result property="createTime" column="create_time" />
|
|||
|
|
<result property="updateTime" column="update_time" />
|
|||
|
|
<result property="remark" column="remark" />
|
|||
|
|
</resultMap>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Use `useGeneratedKeys="true"` and `keyProperty` for inserts.
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Create basic services**
|
|||
|
|
|
|||
|
|
`IFrontProjectService` must expose:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
FrontProject createProject(Long userId, FrontProjectCreateRequest request);
|
|||
|
|
List<FrontProject> listProjects(Long userId);
|
|||
|
|
FrontProject getProject(Long userId, Long projectId);
|
|||
|
|
int updateProject(Long userId, Long projectId, FrontProjectUpdateRequest request);
|
|||
|
|
int deleteProject(Long userId, Long projectId);
|
|||
|
|
DatabaseDesignResponse getDatabase(Long userId, Long projectId);
|
|||
|
|
DatabaseDesignResponse saveDatabase(Long userId, Long projectId, DatabaseDesignResponse request);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`saveDatabase` must delete old `front_project_column`, then old `front_project_table`, then insert the submitted table/column tree in one transaction.
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Write converter test first**
|
|||
|
|
|
|||
|
|
Create `FrontProjectConverterTest` with this test:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Test
|
|||
|
|
public void convertsFrontDraftToGenProject()
|
|||
|
|
{
|
|||
|
|
FrontProject project = new FrontProject();
|
|||
|
|
project.setProjectId(10L);
|
|||
|
|
project.setProjectName("车辆管理系统");
|
|||
|
|
project.setProjectFileName("vehicle-system");
|
|||
|
|
project.setPackageName("com.easycode.vehicle");
|
|||
|
|
project.setVersion("1.0.0");
|
|||
|
|
project.setFrontFramework("Vue3 + Element Plus");
|
|||
|
|
project.setBackFramework("Spring Boot + MyBatis");
|
|||
|
|
|
|||
|
|
FrontProjectTable table = new FrontProjectTable();
|
|||
|
|
table.setTableId(20L);
|
|||
|
|
table.setTableName("car_info");
|
|||
|
|
table.setTableComment("车辆信息表");
|
|||
|
|
table.setClassName("CarInfo");
|
|||
|
|
table.setModuleName("vehicle");
|
|||
|
|
table.setBusinessName("car");
|
|||
|
|
table.setFunctionName("车辆信息");
|
|||
|
|
|
|||
|
|
FrontProjectColumn column = new FrontProjectColumn();
|
|||
|
|
column.setColumnId(30L);
|
|||
|
|
column.setColumnName("car_id");
|
|||
|
|
column.setColumnComment("车辆ID");
|
|||
|
|
column.setColumnType("bigint(20)");
|
|||
|
|
column.setJavaType("Long");
|
|||
|
|
column.setJavaField("carId");
|
|||
|
|
column.setIsPk("1");
|
|||
|
|
column.setIsIncrement("1");
|
|||
|
|
table.setColumns(Collections.singletonList(column));
|
|||
|
|
project.setTables(Collections.singletonList(table));
|
|||
|
|
|
|||
|
|
GenProject genProject = FrontProjectConverter.toGenProject(project);
|
|||
|
|
|
|||
|
|
assertEquals("车辆管理系统", genProject.getProjectName());
|
|||
|
|
assertEquals("com.easycode.vehicle", genProject.getPackageName());
|
|||
|
|
assertEquals(1, genProject.getTables().size());
|
|||
|
|
assertEquals("car_info", genProject.getTables().get(0).getTableName());
|
|||
|
|
assertEquals("carId", genProject.getTables().get(0).getColumns().get(0).getJavaField());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Run test and verify failure**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-generator -am -Dtest=FrontProjectConverterTest test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected before converter implementation:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Compilation failure: cannot find symbol FrontProjectConverter
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Implement `FrontProjectConverter`**
|
|||
|
|
|
|||
|
|
Implement exact conversion rules:
|
|||
|
|
|
|||
|
|
- `FrontProject.projectId` maps to `GenProject.projectId`.
|
|||
|
|
- `FrontProject.projectName` maps to `GenProject.projectName`.
|
|||
|
|
- `FrontProject.projectFileName` maps to `GenProject.projectFileName`.
|
|||
|
|
- `FrontProject.packageName` maps to `GenProject.packageName`.
|
|||
|
|
- `FrontProject.version` maps to `GenProject.version`.
|
|||
|
|
- `FrontProject.frontFramework` maps to `GenProject.frontFramework`.
|
|||
|
|
- `FrontProject.backFramework` maps to `GenProject.backFramework`.
|
|||
|
|
- Each `FrontProjectTable` maps to `GenTable`.
|
|||
|
|
- Each `FrontProjectColumn` maps to `GenTableColumn`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: Run tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-generator -am -Dtest=FrontProjectConverterTest test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Tests run: 1, Failures: 0, Errors: 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 9: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front `
|
|||
|
|
ruoyi-generator/src/main/resources/mapper/front `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front `
|
|||
|
|
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectConverterTest.java
|
|||
|
|
git commit -m "Add front project draft model"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 4: Add Front Authentication APIs
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `ruoyi-admin/src/main/java/com/ruoyi/web/service/front/FrontAuthService.java`
|
|||
|
|
- Create: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontAuthController.java`
|
|||
|
|
- Create DTOs if not already created:
|
|||
|
|
- `FrontLoginBody.java`
|
|||
|
|
- `FrontRegisterBody.java`
|
|||
|
|
- Modify: `IFrontUserService.java`, `FrontUserServiceImpl.java`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add front user service methods**
|
|||
|
|
|
|||
|
|
`IFrontUserService` must include:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
FrontUser selectByUsername(String username);
|
|||
|
|
FrontUser selectById(Long userId);
|
|||
|
|
int register(FrontRegisterBody body);
|
|||
|
|
int updateLastLoginTime(Long userId);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Registration rules:
|
|||
|
|
|
|||
|
|
- username required, length 2 to 50
|
|||
|
|
- password required, length 5 to 50
|
|||
|
|
- duplicate username raises `ServiceException("前台账号已存在")`
|
|||
|
|
- password stored with `SecurityUtils.encryptPassword`
|
|||
|
|
- default nickname is username
|
|||
|
|
- default status is `"0"`
|
|||
|
|
- default delFlag is `"0"`
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Implement front auth service**
|
|||
|
|
|
|||
|
|
`FrontAuthService.login` must:
|
|||
|
|
|
|||
|
|
1. Load front user by username.
|
|||
|
|
2. Reject missing user, disabled user, deleted user, and wrong password.
|
|||
|
|
3. Build a `SysUser` object only for token compatibility.
|
|||
|
|
4. Build a `LoginUser`.
|
|||
|
|
5. Set `loginType` to `Constants.LOGIN_TYPE_FRONT`.
|
|||
|
|
6. Set permissions to `Collections.emptySet()`.
|
|||
|
|
7. Call `tokenService.createToken(loginUser)`.
|
|||
|
|
8. Update `last_login_time`.
|
|||
|
|
|
|||
|
|
Core snippet:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
SysUser tokenUser = new SysUser();
|
|||
|
|
tokenUser.setUserId(frontUser.getUserId());
|
|||
|
|
tokenUser.setUserName(frontUser.getUsername());
|
|||
|
|
tokenUser.setNickName(frontUser.getNickname());
|
|||
|
|
tokenUser.setPassword(frontUser.getPassword());
|
|||
|
|
|
|||
|
|
LoginUser loginUser = new LoginUser(frontUser.getUserId(), null, tokenUser, Collections.emptySet());
|
|||
|
|
loginUser.setLoginType(Constants.LOGIN_TYPE_FRONT);
|
|||
|
|
String token = tokenService.createToken(loginUser);
|
|||
|
|
frontUserService.updateLastLoginTime(frontUser.getUserId());
|
|||
|
|
return token;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Implement controller**
|
|||
|
|
|
|||
|
|
`FrontAuthController` endpoints:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/front/auth")
|
|||
|
|
public class FrontAuthController extends BaseController
|
|||
|
|
{
|
|||
|
|
@PostMapping("/register")
|
|||
|
|
public AjaxResult register(@RequestBody FrontRegisterBody body)
|
|||
|
|
|
|||
|
|
@PostMapping("/login")
|
|||
|
|
public AjaxResult login(@RequestBody FrontLoginBody body)
|
|||
|
|
|
|||
|
|
@GetMapping("/profile")
|
|||
|
|
public AjaxResult profile()
|
|||
|
|
|
|||
|
|
@PostMapping("/logout")
|
|||
|
|
public AjaxResult logout()
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`login` returns:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
AjaxResult ajax = AjaxResult.success();
|
|||
|
|
ajax.put(Constants.TOKEN, token);
|
|||
|
|
return ajax;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`profile` returns the front user from `FrontUserService.selectById(SecurityUtils.getUserId())`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Add service test**
|
|||
|
|
|
|||
|
|
Create `ruoyi-admin/src/test/java/com/ruoyi/web/service/front/FrontAuthServiceTest.java`. Add test dependencies to `ruoyi-admin/pom.xml` if missing:
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<dependency>
|
|||
|
|
<groupId>junit</groupId>
|
|||
|
|
<artifactId>junit</artifactId>
|
|||
|
|
<scope>test</scope>
|
|||
|
|
</dependency>
|
|||
|
|
<dependency>
|
|||
|
|
<groupId>org.mockito</groupId>
|
|||
|
|
<artifactId>mockito-core</artifactId>
|
|||
|
|
<scope>test</scope>
|
|||
|
|
</dependency>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Test:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Test
|
|||
|
|
public void loginCreatesFrontToken()
|
|||
|
|
{
|
|||
|
|
FrontUser user = new FrontUser();
|
|||
|
|
user.setUserId(100L);
|
|||
|
|
user.setUsername("demo");
|
|||
|
|
user.setNickname("Demo");
|
|||
|
|
user.setPassword(SecurityUtils.encryptPassword("admin123"));
|
|||
|
|
user.setStatus("0");
|
|||
|
|
user.setDelFlag("0");
|
|||
|
|
|
|||
|
|
when(frontUserService.selectByUsername("demo")).thenReturn(user);
|
|||
|
|
when(tokenService.createToken(any(LoginUser.class))).thenAnswer(invocation -> {
|
|||
|
|
LoginUser loginUser = invocation.getArgument(0);
|
|||
|
|
assertEquals(Constants.LOGIN_TYPE_FRONT, loginUser.getLoginType());
|
|||
|
|
assertEquals(Long.valueOf(100L), loginUser.getUserId());
|
|||
|
|
return "front-token";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
String token = frontAuthService.login("demo", "admin123");
|
|||
|
|
|
|||
|
|
assertEquals("front-token", token);
|
|||
|
|
verify(frontUserService).updateLastLoginTime(100L);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Run tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-admin -am -Dtest=FrontAuthServiceTest test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Tests run: 1, Failures: 0, Errors: 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontAuthController.java `
|
|||
|
|
ruoyi-admin/src/main/java/com/ruoyi/web/service/front/FrontAuthService.java `
|
|||
|
|
ruoyi-admin/src/test/java/com/ruoyi/web/service/front/FrontAuthServiceTest.java `
|
|||
|
|
ruoyi-admin/pom.xml `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontLoginBody.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontRegisterBody.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontUserService.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontUserServiceImpl.java
|
|||
|
|
git commit -m "Add front user authentication"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 5: Add Project Draft APIs and Editable Database Save
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java`
|
|||
|
|
- Modify: `IFrontProjectService.java`
|
|||
|
|
- Modify: `FrontProjectServiceImpl.java`
|
|||
|
|
- Create/modify DTO classes for project create/update/database response.
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Implement project CRUD controller**
|
|||
|
|
|
|||
|
|
Controller methods:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/front/project")
|
|||
|
|
public class FrontProjectController extends BaseController
|
|||
|
|
{
|
|||
|
|
@Autowired
|
|||
|
|
private IFrontProjectService frontProjectService;
|
|||
|
|
|
|||
|
|
@PostMapping("/create")
|
|||
|
|
public AjaxResult create(@RequestBody FrontProjectCreateRequest request)
|
|||
|
|
|
|||
|
|
@GetMapping("/list")
|
|||
|
|
public AjaxResult list()
|
|||
|
|
|
|||
|
|
@GetMapping("/{projectId}")
|
|||
|
|
public AjaxResult get(@PathVariable Long projectId)
|
|||
|
|
|
|||
|
|
@PutMapping("/{projectId}")
|
|||
|
|
public AjaxResult update(@PathVariable Long projectId, @RequestBody FrontProjectUpdateRequest request)
|
|||
|
|
|
|||
|
|
@DeleteMapping("/{projectId}")
|
|||
|
|
public AjaxResult delete(@PathVariable Long projectId)
|
|||
|
|
|
|||
|
|
@GetMapping("/{projectId}/database")
|
|||
|
|
public AjaxResult database(@PathVariable Long projectId)
|
|||
|
|
|
|||
|
|
@PutMapping("/{projectId}/database")
|
|||
|
|
public AjaxResult saveDatabase(@PathVariable Long projectId, @RequestBody DatabaseDesignResponse request)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Every method gets `Long userId = SecurityUtils.getUserId()` and only touches rows owned by that `userId`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Implement project create defaults**
|
|||
|
|
|
|||
|
|
`FrontProjectServiceImpl.createProject` must default:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
projectFileName = lower hyphen form of projectName, e.g. "车辆管理系统" -> "easycode-project"
|
|||
|
|
packageName = request.packageName if present else "com.easycode.generated"
|
|||
|
|
version = request.version if present else "1.0.0"
|
|||
|
|
frontFramework = "Vue3 + Element Plus"
|
|||
|
|
backFramework = "Spring Boot + MyBatis"
|
|||
|
|
generateStatus = "0"
|
|||
|
|
previewStatus = "0"
|
|||
|
|
status = "0"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
When Chinese project names cannot be slugified safely, use `"easycode-project-" + System.currentTimeMillis()`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Implement `getDatabase`**
|
|||
|
|
|
|||
|
|
Return:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
DatabaseDesignResponse response = new DatabaseDesignResponse();
|
|||
|
|
response.setProjectId(projectId);
|
|||
|
|
response.setTables(tableDesigns);
|
|||
|
|
response.setSql(joinedCreateTableSql);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Join SQL with two line breaks between tables.
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Implement `saveDatabase` validation**
|
|||
|
|
|
|||
|
|
Before saving:
|
|||
|
|
|
|||
|
|
- project must belong to user
|
|||
|
|
- at least one table
|
|||
|
|
- every table name matches `^[a-z][a-z0-9_]{1,63}$`
|
|||
|
|
- every table has at least one column
|
|||
|
|
- every column name matches `^[a-z][a-z0-9_]{1,63}$`
|
|||
|
|
- every column type passes `AiDatabaseSchemaValidator.isAllowedColumnType`
|
|||
|
|
- exactly one primary key per table
|
|||
|
|
|
|||
|
|
Raise `ServiceException` with messages:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
项目不存在或无权限
|
|||
|
|
至少需要一张数据表
|
|||
|
|
表名只能包含小写字母、数字和下划线
|
|||
|
|
每张表至少需要一个字段
|
|||
|
|
字段名只能包含小写字母、数字和下划线
|
|||
|
|
字段类型不在允许范围内
|
|||
|
|
每张表必须且只能有一个主键
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Run generator tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-generator -am -DfailIfNoTests=false test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
[INFO] BUILD SUCCESS
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java
|
|||
|
|
git commit -m "Add front project draft APIs"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 6: Add DeepSeek-backed AI Database Generation
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java`
|
|||
|
|
- Create/modify: `IAiGenerateService.java`, `AiGenerateServiceImpl.java`, `IDeepSeekClient.java`, `HttpDeepSeekClient.java`, `AiDatabaseSchemaValidator.java`, `MysqlDdlBuilder.java`
|
|||
|
|
- Modify: `FrontProjectController.java`
|
|||
|
|
- Modify: `application.yml`
|
|||
|
|
- Test: `AiDatabaseSchemaValidatorTest.java`, `MysqlDdlBuilderTest.java`, `AiGenerateServiceImplTest.java`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add DeepSeek config properties**
|
|||
|
|
|
|||
|
|
Create:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Component
|
|||
|
|
@ConfigurationProperties(prefix = "deepseek")
|
|||
|
|
public class DeepSeekProperties
|
|||
|
|
{
|
|||
|
|
private String apiKey;
|
|||
|
|
private String baseUrl = "https://api.deepseek.com";
|
|||
|
|
private String model = "deepseek-chat";
|
|||
|
|
private Integer timeout = 60000;
|
|||
|
|
private Integer maxTables = 12;
|
|||
|
|
private Integer maxColumnsPerTable = 30;
|
|||
|
|
|
|||
|
|
// getters and setters
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Add to `application.yml`:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
deepseek:
|
|||
|
|
apiKey: ${DEEPSEEK_API_KEY:}
|
|||
|
|
baseUrl: https://api.deepseek.com
|
|||
|
|
model: deepseek-chat
|
|||
|
|
timeout: 60000
|
|||
|
|
maxTables: 12
|
|||
|
|
maxColumnsPerTable: 30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Implement DeepSeek client**
|
|||
|
|
|
|||
|
|
`IDeepSeekClient`:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public interface IDeepSeekClient
|
|||
|
|
{
|
|||
|
|
String chat(String prompt);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`HttpDeepSeekClient` uses `RestTemplate`. Request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"model": "deepseek-chat",
|
|||
|
|
"messages": [
|
|||
|
|
{"role": "system", "content": "你是企业管理系统数据库设计助手,只返回JSON。"},
|
|||
|
|
{"role": "user", "content": "<prompt>"}
|
|||
|
|
],
|
|||
|
|
"temperature": 0.2
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Response parser returns `choices[0].message.content`. If `apiKey` is blank, throw:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
throw new ServiceException("DeepSeek API Key 未配置");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Implement validator**
|
|||
|
|
|
|||
|
|
Allowed type patterns:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
private static final Pattern NAME = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
|
|||
|
|
private static final Pattern BIGINT = Pattern.compile("^bigint(\\(20\\))?$");
|
|||
|
|
private static final Pattern INT = Pattern.compile("^int(\\(11\\))?$");
|
|||
|
|
private static final Pattern VARCHAR = Pattern.compile("^varchar\\(([1-9][0-9]{0,2}|1000)\\)$");
|
|||
|
|
private static final Pattern CHAR = Pattern.compile("^char\\(([1-9][0-9]?)\\)$");
|
|||
|
|
private static final Pattern DECIMAL = Pattern.compile("^decimal\\(([1-9][0-9]?),([1-9][0-9]?)\\)$");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Also allow exact:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
datetime, date, text, longtext
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Reject SQL containing case-insensitive:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
drop, truncate, alter user, grant, revoke, load_file, into outfile
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Implement DDL builder**
|
|||
|
|
|
|||
|
|
For each table produce:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `car_info` (
|
|||
|
|
`car_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '车辆ID',
|
|||
|
|
`car_no` varchar(30) NOT NULL COMMENT '车牌号',
|
|||
|
|
PRIMARY KEY (`car_id`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆信息表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Rules:
|
|||
|
|
|
|||
|
|
- primary key with `isIncrement == "1"` gets `AUTO_INCREMENT`
|
|||
|
|
- required field gets `NOT NULL`
|
|||
|
|
- optional field gets `DEFAULT NULL`
|
|||
|
|
- string comments escape single quote as doubled single quote
|
|||
|
|
- one DDL string per table saved to `FrontProjectTable.createTableSql`
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Write validator tests**
|
|||
|
|
|
|||
|
|
Test cases:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
assertTrue(validator.isValidName("car_info"));
|
|||
|
|
assertFalse(validator.isValidName("CarInfo"));
|
|||
|
|
assertFalse(validator.isValidName("sys user"));
|
|||
|
|
assertTrue(validator.isAllowedColumnType("varchar(100)"));
|
|||
|
|
assertTrue(validator.isAllowedColumnType("decimal(10,2)"));
|
|||
|
|
assertFalse(validator.isAllowedColumnType("json"));
|
|||
|
|
assertFalse(validator.isSafeSql("drop table sys_user"));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Write AI service test with mocked client**
|
|||
|
|
|
|||
|
|
Mock response:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"tables": [
|
|||
|
|
{
|
|||
|
|
"tableName": "car_info",
|
|||
|
|
"tableComment": "车辆信息表",
|
|||
|
|
"columns": [
|
|||
|
|
{
|
|||
|
|
"columnName": "car_id",
|
|||
|
|
"columnType": "bigint(20)",
|
|||
|
|
"javaType": "Long",
|
|||
|
|
"javaField": "carId",
|
|||
|
|
"isPk": "1",
|
|||
|
|
"isIncrement": "1",
|
|||
|
|
"isRequired": "1",
|
|||
|
|
"columnComment": "车辆ID"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"columnName": "car_no",
|
|||
|
|
"columnType": "varchar(30)",
|
|||
|
|
"javaType": "String",
|
|||
|
|
"javaField": "carNo",
|
|||
|
|
"isPk": "0",
|
|||
|
|
"isIncrement": "0",
|
|||
|
|
"isRequired": "1",
|
|||
|
|
"columnComment": "车牌号"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
- inserts one `front_project_table`
|
|||
|
|
- inserts two `front_project_column`
|
|||
|
|
- saves one generation record with `success = "1"`
|
|||
|
|
- returns SQL containing `CREATE TABLE \`car_info\``
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Add controller endpoint**
|
|||
|
|
|
|||
|
|
In `FrontProjectController`:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@PostMapping("/{projectId}/generate-database")
|
|||
|
|
public AjaxResult generateDatabase(@PathVariable Long projectId, @RequestBody GenerateDatabaseRequest request)
|
|||
|
|
{
|
|||
|
|
Long userId = SecurityUtils.getUserId();
|
|||
|
|
return success(aiGenerateService.generateDatabase(userId, projectId, request));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: Run tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-generator -am -Dtest=AiDatabaseSchemaValidatorTest,MysqlDdlBuilderTest,AiGenerateServiceImplTest test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Tests run: at least 3, Failures: 0, Errors: 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 9: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-admin/src/main/resources/application.yml `
|
|||
|
|
ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front `
|
|||
|
|
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front
|
|||
|
|
git commit -m "Add DeepSeek database generation service"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 7: Adapt Generator Preview for Front Drafts
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `IGenProjectService.java`
|
|||
|
|
- Modify: `GenProjectServiceImpl.java`
|
|||
|
|
- Create/modify: `IFrontProjectPreviewService.java`, `FrontProjectPreviewServiceImpl.java`
|
|||
|
|
- Modify: `FrontProjectController.java`
|
|||
|
|
- Test: `FrontProjectPreviewServiceImplTest.java`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add generator overloads**
|
|||
|
|
|
|||
|
|
In `IGenProjectService`, add:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
List<Map<String, Object>> getProjectStructure(GenProject project, String type);
|
|||
|
|
|
|||
|
|
Map<String, String> previewCode(GenProject project, Long tableId, String keyword, String type);
|
|||
|
|
|
|||
|
|
byte[] downloadStructure(GenProject project, String type);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Existing ID-based methods stay unchanged and delegate to the new object-based overload after loading `GenProject`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Refactor `GenProjectServiceImpl`**
|
|||
|
|
|
|||
|
|
The object overload must not query `gen_project` by ID. It may still query active templates and project structures.
|
|||
|
|
|
|||
|
|
Template resolution rules:
|
|||
|
|
|
|||
|
|
1. If a project has a `sys_project_template` row for `project.projectId` and `type`, use it.
|
|||
|
|
2. Otherwise choose the first active `sys_template` where `template_type = type`.
|
|||
|
|
3. If none exists, throw `ServiceException("未配置可用模板:" + type)`.
|
|||
|
|
|
|||
|
|
Structure query rules:
|
|||
|
|
|
|||
|
|
- For admin projects, keep existing project template structure lookup.
|
|||
|
|
- For front draft projects without `sys_project_template`, use `sys_project_structure` nodes for the resolved template ID.
|
|||
|
|
- Expand table-scoped nodes for every `GenTable` in `project.getTables()`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Implement front preview service**
|
|||
|
|
|
|||
|
|
`IFrontProjectPreviewService`:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
List<Map<String, Object>> getStructure(Long userId, Long projectId, String templateType);
|
|||
|
|
Map<String, String> getFileContent(Long userId, Long projectId, PreviewFileRequest request);
|
|||
|
|
byte[] download(Long userId, Long projectId, String templateType);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Implementation:
|
|||
|
|
|
|||
|
|
1. Load `FrontProject` with tables and columns by user/project ID.
|
|||
|
|
2. Convert to `GenProject`.
|
|||
|
|
3. Call `genProjectService.getProjectStructure(genProject, templateType)`.
|
|||
|
|
4. Call `genProjectService.previewCode(genProject, tableId, category, templateType)` for file content.
|
|||
|
|
5. Call `genProjectService.downloadStructure(genProject, templateType)` for zip.
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Add preview endpoints**
|
|||
|
|
|
|||
|
|
In `FrontProjectController`:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@PostMapping("/{projectId}/preview")
|
|||
|
|
public AjaxResult preview(@PathVariable Long projectId)
|
|||
|
|
|
|||
|
|
@GetMapping("/{projectId}/structure")
|
|||
|
|
public AjaxResult structure(@PathVariable Long projectId, @RequestParam String templateType)
|
|||
|
|
|
|||
|
|
@GetMapping("/{projectId}/file-content")
|
|||
|
|
public AjaxResult fileContent(@PathVariable Long projectId, PreviewFileRequest request)
|
|||
|
|
|
|||
|
|
@GetMapping("/{projectId}/download")
|
|||
|
|
public void download(@PathVariable Long projectId, @RequestParam String templateType, HttpServletResponse response)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`preview` marks `front_project.preview_status = "1"` after structure can be generated for `backend`, `frontend`, and `admin_frontend`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Write preview service test**
|
|||
|
|
|
|||
|
|
Test:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Test
|
|||
|
|
public void returnsStructureForDraftProject()
|
|||
|
|
{
|
|||
|
|
when(frontProjectService.getProject(100L, 200L)).thenReturn(frontProjectWithOneTable());
|
|||
|
|
when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
|
|||
|
|
.thenReturn(Collections.singletonList(Collections.singletonMap("label", "backend")));
|
|||
|
|
|
|||
|
|
List<Map<String, Object>> structure = previewService.getStructure(100L, 200L, "backend");
|
|||
|
|
|
|||
|
|
assertEquals("backend", structure.get(0).get("label"));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Run tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-generator -am -Dtest=FrontProjectPreviewServiceImplTest,GenProjectServiceImplTest test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Tests run: existing generator tests plus preview test, Failures: 0, Errors: 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenProjectService.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectPreviewService.java `
|
|||
|
|
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java `
|
|||
|
|
ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java `
|
|||
|
|
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java
|
|||
|
|
git commit -m "Support front draft project preview"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 8: Scaffold Independent `easycode-web`
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create all `easycode-web` files listed in File Map.
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Create package and Vite config**
|
|||
|
|
|
|||
|
|
`easycode-web/package.json`:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "easycode-web",
|
|||
|
|
"version": "1.0.0",
|
|||
|
|
"private": true,
|
|||
|
|
"type": "module",
|
|||
|
|
"scripts": {
|
|||
|
|
"dev": "vite --host 0.0.0.0",
|
|||
|
|
"build": "vite build",
|
|||
|
|
"preview": "vite preview --host 0.0.0.0"
|
|||
|
|
},
|
|||
|
|
"dependencies": {
|
|||
|
|
"@element-plus/icons-vue": "^2.3.1",
|
|||
|
|
"axios": "^1.7.9",
|
|||
|
|
"element-plus": "^2.9.1",
|
|||
|
|
"vue": "^3.5.13",
|
|||
|
|
"vue-router": "^4.5.0"
|
|||
|
|
},
|
|||
|
|
"devDependencies": {
|
|||
|
|
"@vitejs/plugin-vue": "^5.2.1",
|
|||
|
|
"sass": "^1.83.0",
|
|||
|
|
"vite": "^6.0.7"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`easycode-web/vite.config.js`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import { defineConfig, loadEnv } from 'vite'
|
|||
|
|
import vue from '@vitejs/plugin-vue'
|
|||
|
|
|
|||
|
|
export default defineConfig(({ mode }) => {
|
|||
|
|
const env = loadEnv(mode, process.cwd())
|
|||
|
|
return {
|
|||
|
|
plugins: [vue()],
|
|||
|
|
server: {
|
|||
|
|
port: 5174,
|
|||
|
|
proxy: {
|
|||
|
|
'/front': {
|
|||
|
|
target: env.VITE_API_BASE_URL || 'http://localhost:8080',
|
|||
|
|
changeOrigin: true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`.env.development`:
|
|||
|
|
|
|||
|
|
```env
|
|||
|
|
VITE_API_BASE_URL=http://localhost:8080
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Create router guard**
|
|||
|
|
|
|||
|
|
`easycode-web/src/router/index.js`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
|||
|
|
import { getToken } from '@/utils/auth'
|
|||
|
|
|
|||
|
|
const routes = [
|
|||
|
|
{ path: '/', name: 'Home', component: () => import('@/views/HomeView.vue') },
|
|||
|
|
{ path: '/login', name: 'Login', component: () => import('@/views/LoginView.vue') },
|
|||
|
|
{ path: '/register', name: 'Register', component: () => import('@/views/RegisterView.vue') },
|
|||
|
|
{ path: '/generate', name: 'Generate', component: () => import('@/views/GenerateView.vue'), meta: { requiresAuth: true } },
|
|||
|
|
{ path: '/project/:projectId/preview', name: 'Preview', component: () => import('@/views/PreviewView.vue'), meta: { requiresAuth: true } }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const router = createRouter({
|
|||
|
|
history: createWebHistory(),
|
|||
|
|
routes
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
router.beforeEach((to) => {
|
|||
|
|
if (to.meta.requiresAuth && !getToken()) {
|
|||
|
|
return { path: '/login', query: { redirect: to.fullPath } }
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default router
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Create request wrapper**
|
|||
|
|
|
|||
|
|
`easycode-web/src/api/request.js`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import axios from 'axios'
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import { getToken, removeToken } from '@/utils/auth'
|
|||
|
|
|
|||
|
|
const service = axios.create({
|
|||
|
|
baseURL: '',
|
|||
|
|
timeout: 60000
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
service.interceptors.request.use((config) => {
|
|||
|
|
const token = getToken()
|
|||
|
|
if (token) {
|
|||
|
|
config.headers.Authorization = `Bearer ${token}`
|
|||
|
|
}
|
|||
|
|
return config
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
service.interceptors.response.use(
|
|||
|
|
(response) => {
|
|||
|
|
const data = response.data
|
|||
|
|
if (data && data.code && data.code !== 200) {
|
|||
|
|
ElMessage.error(data.msg || '请求失败')
|
|||
|
|
return Promise.reject(new Error(data.msg || '请求失败'))
|
|||
|
|
}
|
|||
|
|
return data
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
if (error.response && error.response.status === 401) {
|
|||
|
|
removeToken()
|
|||
|
|
ElMessage.error('登录已过期,请重新登录')
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(error.message || '网络异常')
|
|||
|
|
}
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
export default service
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Create home page**
|
|||
|
|
|
|||
|
|
Use the prototype as content reference but write clean Vue and Chinese text. Keep the first viewport as the usable generation entry, not a marketing-only page.
|
|||
|
|
|
|||
|
|
Hero actions:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const startGenerate = () => {
|
|||
|
|
router.push({
|
|||
|
|
path: '/generate',
|
|||
|
|
query: { keyword: keyword.value }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Login action:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
router.push('/login')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Create generate page with mock fallback**
|
|||
|
|
|
|||
|
|
`GenerateView.vue` must support these states:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const activeStep = ref(0)
|
|||
|
|
const generating = ref(false)
|
|||
|
|
const projectId = ref(null)
|
|||
|
|
const database = ref({ tables: [], sql: '' })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Buttons:
|
|||
|
|
|
|||
|
|
- “生成数据库” calls `createProject`, then `generateDatabase`.
|
|||
|
|
- “保存表结构” calls `saveDatabase`.
|
|||
|
|
- “预览项目” routes to `/project/${projectId}/preview`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Create preview page**
|
|||
|
|
|
|||
|
|
`PreviewView.vue` loads three structures:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const templateTypes = [
|
|||
|
|
{ label: '后端项目', value: 'backend' },
|
|||
|
|
{ label: '前台前端', value: 'frontend' },
|
|||
|
|
{ label: '后台前端', value: 'admin_frontend' }
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Clicking a file node calls:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
getFileContent(projectId, {
|
|||
|
|
templateType: activeTemplateType.value,
|
|||
|
|
nodeId: node.id,
|
|||
|
|
tableId: node.tableId,
|
|||
|
|
category: node.category
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Install and build**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
cd easycode-web
|
|||
|
|
npm install
|
|||
|
|
npm run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
vite build succeeds and creates easycode-web/dist
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If `npm install` fails because network access is blocked, rerun with escalation and keep the same command.
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add easycode-web
|
|||
|
|
git commit -m "Add EasyCode front web app"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 9: Wire Front App to Real APIs
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `easycode-web/src/api/auth.js`
|
|||
|
|
- Modify: `easycode-web/src/api/project.js`
|
|||
|
|
- Modify: `easycode-web/src/views/LoginView.vue`
|
|||
|
|
- Modify: `easycode-web/src/views/RegisterView.vue`
|
|||
|
|
- Modify: `easycode-web/src/views/GenerateView.vue`
|
|||
|
|
- Modify: `easycode-web/src/views/PreviewView.vue`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Implement auth API**
|
|||
|
|
|
|||
|
|
`auth.js`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import request from './request'
|
|||
|
|
|
|||
|
|
export function login(data) {
|
|||
|
|
return request.post('/front/auth/login', data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function register(data) {
|
|||
|
|
return request.post('/front/auth/register', data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function profile() {
|
|||
|
|
return request.get('/front/auth/profile')
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Implement project API**
|
|||
|
|
|
|||
|
|
`project.js`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import request from './request'
|
|||
|
|
|
|||
|
|
function unwrap(res) {
|
|||
|
|
return res && Object.prototype.hasOwnProperty.call(res, 'data') ? res.data : res
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function createProject(data) {
|
|||
|
|
return request.post('/front/project/create', data).then(unwrap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function generateDatabase(projectId, data) {
|
|||
|
|
return request.post(`/front/project/${projectId}/generate-database`, data).then(unwrap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getDatabase(projectId) {
|
|||
|
|
return request.get(`/front/project/${projectId}/database`).then(unwrap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function saveDatabase(projectId, data) {
|
|||
|
|
return request.put(`/front/project/${projectId}/database`, data).then(unwrap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getStructure(projectId, templateType) {
|
|||
|
|
return request.get(`/front/project/${projectId}/structure`, { params: { templateType } }).then(unwrap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getFileContent(projectId, params) {
|
|||
|
|
return request.get(`/front/project/${projectId}/file-content`, { params }).then(unwrap)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Use token from login**
|
|||
|
|
|
|||
|
|
On successful login:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const res = await login(form)
|
|||
|
|
setToken(res.token)
|
|||
|
|
router.push(route.query.redirect || '/generate')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Wire generate page**
|
|||
|
|
|
|||
|
|
On “生成数据库”:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const project = await createProject({
|
|||
|
|
projectName: form.projectName,
|
|||
|
|
projectDesc: form.projectDesc,
|
|||
|
|
industryTemplate: form.industryTemplate
|
|||
|
|
})
|
|||
|
|
projectId.value = project.projectId
|
|||
|
|
const result = await generateDatabase(projectId.value, form)
|
|||
|
|
database.value = result
|
|||
|
|
activeStep.value = 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The `unwrap` helper in `project.js` normalizes `AjaxResult.success(data)` to the inner `data` object.
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Wire preview page**
|
|||
|
|
|
|||
|
|
Load structures after route enter:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
for (const item of templateTypes) {
|
|||
|
|
const res = await getStructure(projectId, item.value)
|
|||
|
|
structures.value[item.value] = res || []
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Click file node:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const res = await getFileContent(projectId, {
|
|||
|
|
templateType: activeTemplateType.value,
|
|||
|
|
nodeId: node.id,
|
|||
|
|
tableId: node.tableId,
|
|||
|
|
category: node.category
|
|||
|
|
})
|
|||
|
|
code.value = res && res.content ? res.content : ''
|
|||
|
|
fileName.value = node.label
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Build front app**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
cd easycode-web
|
|||
|
|
npm run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
vite build succeeds
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add easycode-web/src
|
|||
|
|
git commit -m "Connect front web app to workbench APIs"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 10: End-to-End Verification and Documentation
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `README.md`
|
|||
|
|
- Create or modify: `easycode-web/README.md`
|
|||
|
|
- Optional create: `sql/front_workbench_test_data.sql`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add front app README**
|
|||
|
|
|
|||
|
|
`easycode-web/README.md` must include:
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
# EasyCode Web
|
|||
|
|
|
|||
|
|
## Start
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
npm install
|
|||
|
|
npm run dev
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Default URL: http://localhost:5174
|
|||
|
|
|
|||
|
|
## Backend
|
|||
|
|
|
|||
|
|
Set `VITE_API_BASE_URL` in `.env.development`.
|
|||
|
|
|
|||
|
|
## Flow
|
|||
|
|
|
|||
|
|
1. Register or login front user.
|
|||
|
|
2. Enter project name and description.
|
|||
|
|
3. Generate MySQL database design through backend DeepSeek service.
|
|||
|
|
4. Edit tables and fields.
|
|||
|
|
5. Preview backend, frontend, and admin frontend project files.
|
|||
|
|
```
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Update root README**
|
|||
|
|
|
|||
|
|
Add a short “前台生成工作台” section with:
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
### 前台生成工作台
|
|||
|
|
|
|||
|
|
- 前台目录:`easycode-web`
|
|||
|
|
- 后台接口前缀:`/front/**`
|
|||
|
|
- DeepSeek 只在后端 `AiGenerateService` 中调用
|
|||
|
|
- 新增数据库脚本:`sql/front_workbench.sql`
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Backend verification**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
mvn -pl ruoyi-admin -am -DfailIfNoTests=false test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
[INFO] BUILD SUCCESS
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Frontend verification**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
cd easycode-web
|
|||
|
|
npm run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
vite build succeeds
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Manual smoke test**
|
|||
|
|
|
|||
|
|
Start backend and front app. Use these checks:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
1. Open http://localhost:5174
|
|||
|
|
2. Click 登录 and register a front user
|
|||
|
|
3. Login redirects back to /generate
|
|||
|
|
4. Create project "车辆管理系统"
|
|||
|
|
5. Click 生成数据库
|
|||
|
|
6. Tables are displayed and editable
|
|||
|
|
7. Save database design
|
|||
|
|
8. Click 预览项目
|
|||
|
|
9. backend/frontend/admin_frontend structures load
|
|||
|
|
10. Click a file and code appears on the right
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git add README.md easycode-web/README.md
|
|||
|
|
git commit -m "Document front workbench setup"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Final Verification Before Merge
|
|||
|
|
|
|||
|
|
Run from repository root:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
git status -sb
|
|||
|
|
git diff --check
|
|||
|
|
mvn -pl ruoyi-admin -am -DfailIfNoTests=false test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Run from `easycode-web`:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
npm run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
working tree clean after final commit
|
|||
|
|
git diff --check has no output
|
|||
|
|
Maven BUILD SUCCESS
|
|||
|
|
Vite build succeeds
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Implementation Notes
|
|||
|
|
|
|||
|
|
- Keep DeepSeek API key only in backend environment/config.
|
|||
|
|
- Do not add DeepSeek calls to `easycode-web`.
|
|||
|
|
- Use front token only for `/front/**`.
|
|||
|
|
- Do not reuse `sys_user` for front users.
|
|||
|
|
- Do not save AI output directly without `AiDatabaseSchemaValidator`.
|
|||
|
|
- Keep generated draft data in `front_*` tables; only convert to `GenProject` in memory for preview/download.
|