一、库表设计

应用表是整个项目的核心,需要记录应用的基本信息、生成配置、部署信息等。
其中最关键的是deployKey字段。由于每个网站应用文件的部署都是隔离的(想象成沙箱),需要用唯一字段来区分,可以作为应用的存储和访问路径;而且为了便于访问,每个应用的访问路径不能太长。
这里我们参考美团NoCode等平台的设计,将deployKey设置为6位英文数字组成的唯一标识符。app表的建表SQL如下:

-- 应用表
create table app
(
    id           bigint auto_increment comment 'id' primary key,
    appName      varchar(256)                       null comment '应用名称',
    cover        varchar(512)                       null comment '应用封面',
    initPrompt   text                               null comment '应用初始化的 prompt',
    codeGenType  varchar(64)                        null comment '代码生成类型(枚举)',
    deployKey    varchar(64)                        null comment '部署标识',
    deployedTime datetime                           null comment '部署时间',
    priority     int      default 0                 not null comment '优先级',
    userId       bigint                             not null comment '创建用户id',
    editTime     datetime default CURRENT_TIMESTAMP not null comment '编辑时间',
    createTime   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete     tinyint  default 0                 not null comment '是否删除',
    UNIQUE KEY uk_deployKey (deployKey), -- 确保部署标识唯一
    INDEX idx_appName (appName),         -- 提升基于应用名称的查询性能
    INDEX idx_userId (userId)            -- 提升基于用户 ID 的查询性能
) comment '应用' collate = utf8mb4_unicode_ci;

这个设计中有几个值得注意的细节:
1) priority优先级字段:我们约定99表示精选应用,这样可以在主页展示高质量的应用,避免用户看到大量测试内容。
为什么用数字而不用枚举类型呢?原因是这样更利于扩展,比如约定 999 表示置顶;还可以根据数字灵活调整各个应用的具体展示顺序。
2)添加索引:给deployKey、appName、userId三个经常用于作为查询条件的字段增加索引,提高查询性能。 
注意,我们暂时不考虑将应用代码直接保存到数据库字段中,而是保存在文件系统里。这样可以避免数据库和文件存储不一致的问题,也便于后续扩展到对象存储等方案。

二、基础代码生成

由于之前已经开发过用户模块,项目内已经有了我们自己的开发风格,可以作为AI的参考了。因此这次我们不再人工编写增删改查代码,而是将明确的需求描述提供给AI,让它来生成业务代码。
在Cursor中打开 后端项目根目录,执行以下提示词:

请参考项目中已有的 User 模块的文件和代码风格,帮我根据下列需求,生成完整的 App 模块的代码。
需要的功能如下:
【用户】创建应用(须填写 initPrompt)
【用户】根据 id 修改自己的应用(目前只支持修改应用名称)
【用户】根据 id 删除自己的应用
【用户】根据 id 查看应用详情 -
【用户】分页查询自己的应用列表(支持根据名称查询,每页最多 20 个)
【用户】分页查询精选的应用列表(支持根据名称查询,每页最多 20 个)
【管理员】根据 id 删除任意应用
【管理员】根据 id 更新任意应用(支持更新应用名称、应用封面、优先级)
【管理员】分页查询应用列表(支持根据除时间外的任何字段查询,每页数量不限)
【管理员】根据 id 查看应用详情

接下来我们需要对照需求,逐一检查和完善生成的代码。

三、核心功能实现

1.创建应用
用户创建应用时,只需要填写初始化提示词。系统会自动生成应用名称,取提示词前12位和默认的代码生成类型。
(1)请求类
@Data
public class AppAddRequest implements Serializable {

    /**
     * 应用初始化的 prompt
     */
    private String initPrompt;

    private static final long serialVersionUID = 1L;
}
(2)接口代码
@PostMapping("/add")
    public BaseResponse<Long> addApp(@RequestBody AppAddRequest appAddRequest, HttpServletRequest request) {

        ThrowUtils.throwIf(appAddRequest == null, ErrorCode.PARAMS_ERROR);

        // 参数校验
        String initPrompt = appAddRequest.getInitPrompt();
        ThrowUtils.throwIf(StrUtil.isBlank(initPrompt), ErrorCode.PARAMS_ERROR, "初始化 prompt 不能为空");

        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);

        // 构造入库对象
        App app = new App();
        BeanUtil.copyProperties(appAddRequest, app);
        app.setUserId(loginUser.getId());

        // 应用名称暂时为 initPrompt 前 12 位
        app.setAppName(initPrompt.substring(0, Math.min(initPrompt.length(), 12)));

        // 暂时设置为多文件生成
        app.setCodeGenType(CodeGenTypeEnum.MULTI_FILE.getValue());

        // 插入数据库
        boolean result = appService.save(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);

        return ResultUtils.success(app.getId());
    }
2.更新应用
(1)请求类
@Data
public class AppUpdateRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 应用名称
     */
    private String appName;

    private static final long serialVersionUID = 1L;
}
(2)接口代码
@PostMapping("/update")
    public BaseResponse<Boolean> updateApp(@RequestBody AppUpdateRequest appUpdateRequest, HttpServletRequest request) {
        if (appUpdateRequest == null || appUpdateRequest.getId() == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        User loginUser = userService.getLoginUser(request);
        long id = appUpdateRequest.getId();

        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);

        // 仅本人可更新
        if (!oldApp.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }

        App app = new App();
        app.setId(id);
        app.setAppName(appUpdateRequest.getAppName());
        // 设置编辑时间
        app.setEditTime(LocalDateTime.now());

        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);

        return ResultUtils.success(true);
    }
这里我们手动设置了editTime ,这是为了区分用户主动编辑和系统自动更新的时间。
3.用户删除应用
接口代码
@PostMapping("/delete")
    public BaseResponse<Boolean> deleteApp(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        User loginUser = userService.getLoginUser(request);
        long id = deleteRequest.getId();

        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);

        // 仅本人或管理员可删除
        if (!oldApp.getUserId().equals(loginUser.getId()) && !UserConstant.ADMIN_ROLE.equals(loginUser.getUserRole())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }

        boolean result = appService.removeById(id);
        return ResultUtils.success(result);
    }
4.用户查看应用详情
(1)App封装类
应用查询涉及到关联查询用户信息,需要创建App的封装类, 包含UserVO用户信息字段:
@Data
public class AppVO implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 应用名称
     */
    private String appName;

    /**
     * 应用封面
     */
    private String cover;

    /**
     * 应用初始化的 prompt
     */
    private String initPrompt;

    /**
     * 代码生成类型(枚举)
     */
    private String codeGenType;

    /**
     * 部署标识
     */
    private String deployKey;

    /**
     * 部署时间
     */
    private LocalDateTime deployedTime;

    /**
     * 优先级
     */
    private Integer priority;

    /**
     * 创建用户id
     */
    private Long userId;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 创建用户信息
     */
    private UserVO user;

    private static final long serialVersionUID = 1L;
}

AppService中补充查询App关联信息的方法:

@Override
    public AppVO getAppVO(App app) {
        if (app == null) {
            return null;
        }
        AppVO appVO = new AppVO();
        BeanUtil.copyProperties(app, appVO);
        // 关联查询用户信息
        Long userId = app.getUserId();
        if (userId != null) {
            User user = userService.getById(userId);
            UserVO userVO = userService.getUserVO(user);
            appVO.setUser(userVO);
        }
        return appVO;
    }
(2)接口代码
先查询App,再查询封装类:
@GetMapping("/get/vo")
    public BaseResponse<AppVO> getAppVOById(long id) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);

        // 查询数据库
        App app = appService.getById(id);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);

        // 获取封装类(包含用户信息)
        return ResultUtils.success(appService.getAppVO(app));
    }
5.用户分页查询应用
(1)请求类

主要定义了可作为查询条件的字段:

@EqualsAndHashCode(callSuper = true)
@Data
public class AppQueryRequest extends PageRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 应用名称
     */
    private String appName;

    /**
     * 应用封面
     */
    private String cover;

    /**
     * 应用初始化的 prompt
     */
    private String initPrompt;

    /**
     * 代码生成类型(枚举)
     */
    private String codeGenType;

    /**
     * 部署标识
     */
    private String deployKey;

    /**
     * 优先级
     */
    private Integer priority;

    /**
     * 创建用户id
     */
    private Long userId;

    private static final long serialVersionUID = 1L;
}
在AppService中添加构造查询对象的方法:
@Override
    public QueryWrapper getQueryWrapper(AppQueryRequest appQueryRequest) {
        if (appQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
        }

        Long id = appQueryRequest.getId();
        String appName = appQueryRequest.getAppName();
        String cover = appQueryRequest.getCover();
        String initPrompt = appQueryRequest.getInitPrompt();
        String codeGenType = appQueryRequest.getCodeGenType();
        String deployKey = appQueryRequest.getDeployKey();
        Integer priority = appQueryRequest.getPriority();
        Long userId = appQueryRequest.getUserId();
        String sortField = appQueryRequest.getSortField();
        String sortOrder = appQueryRequest.getSortOrder();

        QueryWrapper wrapper = QueryWrapper.create();

        // 只有 有值 才拼接条件
        if (id != null && id > 0) {
            wrapper.eq("id", id);
        }
        if (StrUtil.isNotBlank(appName)) {
            wrapper.like("appName", appName);
        }
        if (StrUtil.isNotBlank(cover)) {
            wrapper.like("cover", cover);
        }
        if (StrUtil.isNotBlank(initPrompt)) {
            wrapper.like("initPrompt", initPrompt);
        }
        if (StrUtil.isNotBlank(codeGenType)) {
            wrapper.eq("codeGenType", codeGenType);
        }
        if (StrUtil.isNotBlank(deployKey)) {
            wrapper.eq("deployKey", deployKey);
        }
        if (priority != null && priority > 0) {
            wrapper.eq("priority", priority);
        }
        if (userId != null && userId > 0) {
            wrapper.eq("userId", userId);
        }

        // 排序
        if (StrUtil.isNotBlank(sortField)) {
            wrapper.orderBy(sortField, "ascend".equals(sortOrder));
        }

        return wrapper;
    }
分页查询应用时,也需要额外获取创建应用的用户信息,这会涉及到关联查询多个用户信息,我们需要优化查询 性能。优化查询逻辑如下:
1.先收集所有userId到集合中
2.根据userId集合批量查询所有用户信息
3.构建Map映射关系userId => UserVO
4.一次性组装所有AppVO,根据userId 从Map中取到需要的用户信息
@Override
    public List<AppVO> getAppVOList(List<App> appList) {
        if (CollUtil.isEmpty(appList)) {
            return new ArrayList<>();
        }
        // 批量获取用户信息,避免 N+1 查询问题
        Set<Long> userIds = appList.stream()
                .map(App::getUserId)
                .collect(Collectors.toSet());
        Map<Long, UserVO> userVOMap = userService.listByIds(userIds).stream()
                .collect(Collectors.toMap(User::getId, userService::getUserVO));
        return appList.stream().map(app -> {
            AppVO appVO = getAppVO(app);
            UserVO userVO = userVOMap.get(app.getUserId());
            appVO.setUser(userVO);
            return appVO;
        }).collect(Collectors.toList());
    }
(2)接口代码
@PostMapping("/my/list/page/vo")
    public BaseResponse<Page<AppVO>> listMyAppVOByPage(@RequestBody AppQueryRequest appQueryRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);

        User loginUser = userService.getLoginUser(request);

        // 限制每页最多 20 个
        long pageSize = appQueryRequest.getPageSize();
        ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");

        long pageNum = appQueryRequest.getPageNum();

        // 只查询当前用户的应用
        //log.info("登录用户ID:{}", loginUser.getId()); // 打印登录用户ID
        appQueryRequest.setUserId(loginUser.getId());
        //log.info("最终查询的userId:{}", appQueryRequest.getUserId()); // 打印最终传入的userId

        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);


        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);

        // 数据封装
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);

        return ResultUtils.success(appVOPage);
    }
6.用户分页查询精选应用
(1)

这里参考了大厂的零代码应用生成平台,用户只能在主页查询精选应用列表(还有自己的),这样主页会更干净 ;同时避免了爬虫,相当于起到 了一个管理员审核的作用。

创建constant包和常量类AppConstant,存储应用优先级常量:

public interface AppConstant {

    /**
     * 精选应用的优先级
     */
    Integer GOOD_APP_PRIORITY = 99;

    /**
     * 默认应用优先级
     */
    Integer DEFAULT_APP_PRIORITY = 0;

}
(2)接口代码
@PostMapping("/good/list/page/vo")
    public BaseResponse<Page<AppVO>> listGoodAppVOByPage(@RequestBody AppQueryRequest appQueryRequest) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);

        // 限制每页最多 20 个
        long pageSize = appQueryRequest.getPageSize();
        ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");

        long pageNum = appQueryRequest.getPageNum();

        // 只查询精选的应用
        appQueryRequest.setPriority(AppConstant.GOOD_APP_PRIORITY);

        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        // 分页查询
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);

        // 数据封装
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);

        return ResultUtils.success(appVOPage);
    }
7.管理员删除应用
跟用户删除应用接口类似,但是管理员可以删除任意应用,可以通过权限注解校验权限:
@PostMapping("/admin/delete")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> deleteAppByAdmin(@RequestBody DeleteRequest deleteRequest) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        long id = deleteRequest.getId();

        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);

        boolean result = appService.removeById(id);
        return ResultUtils.success(result);
    }
8.管理员更新应用
(1)请求类
管理员可以更新任意应用的应用名称、应用封面和优先级,更新优先级的操作其实就是精选
@Data
public class AppAdminUpdateRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 应用名称
     */
    private String appName;

    /**
     * 应用封面
     */
    private String cover;

    /**
     * 优先级
     */
    private Integer priority;

    private static final long serialVersionUID = 1L;
}
(2)接口代码
@PostMapping("/admin/update")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> updateAppByAdmin(@RequestBody AppAdminUpdateRequest appAdminUpdateRequest) {
        if (appAdminUpdateRequest == null || appAdminUpdateRequest.getId() == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        long id = appAdminUpdateRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);

        App app = new App();
        BeanUtil.copyProperties(appAdminUpdateRequest, app);
        // 设置编辑时间
        app.setEditTime(LocalDateTime.now());

        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);

        return ResultUtils.success(true);
    }
9.管理员分页查询应用
管理员比普通用户拥有更大的查询范围,支持根据除时间外的任何字段查询,并且每页数量不限
@PostMapping("/admin/list/page/vo")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Page<AppVO>> listAppVOByPageByAdmin(@RequestBody AppQueryRequest appQueryRequest) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);

        long pageNum = appQueryRequest.getPageNum();
        long pageSize = appQueryRequest.getPageSize();

        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);

        // 数据封装
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);

        return ResultUtils.success(appVOPage);
    }
10.管理员查看应用详情
这个接口除了权限之外,目前跟用户查看应用详情接口没有区别:
@GetMapping("/admin/get/vo")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<AppVO> getAppVOByIdByAdmin(long id) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);

        // 查询数据库
        App app = appService.getById(id);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);

        // 获取封装类
        return ResultUtils.success(appService.getAppVO(app));
    }

四、收获

在开发实践中,熟练掌握了基于现有项目规范快速开发 CRUD 接口的流程,学会借助 AI 工具辅助代码生成,并结合业务需求人工校验、优化代码,大幅提升开发效率。同时深入理解权限控制逻辑,区分普通用户与管理员不同操作权限,掌握注解式权限校验的实际用法。

此外,我认识到后端开发中数据查询优化、接口封装、数据隔离的重要性,学会规避 N+1 查询等常见问题,培养了规范化、严谨化的编码思维,提升了问题分析与功能落地的综合实践能力。

Logo

一站式 AI 云服务平台

更多推荐