CLAUDE.md 18 KB

CLAUDE.md

项目概述

项目名称 jeeplus
技术栈 Jeeplus + Spring 4.0.8 + MyBatis 3.2.8 + MySQL 5.7 + Layui 2.0.3
打包方式 WAR (jeeplus-0.0.1-SNAPSHOT.war)
Java JDK 7/8
ORM MyBatis 3.2.8,XML 映射文件
前端 JSP + Layui 2.0.3 + jQuery,自定义 jeeplus 标签库
工作流 Activiti 5.x
权限 Apache Shiro 1.2.3
代码仓库 Git,分支 master

核心配置文件

  • jeeplus.properties — 主配置(数据库、adminPath=/a、frontPath=/f 等)
  • pom.xml — Maven 依赖管理
  • Spring 配置位于 src/main/resources/spring/ 目录

项目包结构

com.jeeplus
├── common/                          # 公共基础类
│   ├── config/Global.java           # 全局配置常量
│   ├── persistence/
│   │   ├── BaseEntity.java          # Entity 基类 (id, currentUser, page, sqlMap, isNewRecord)
│   │   ├── DataEntity.java          # 数据实体基类 (remarks, createBy, createDate, updateBy, updateDate, delFlag)
│   │   ├── ActEntity.java           # 工作流实体基类 (扩展 DataEntity, 含 Act act)
│   │   ├── CrudDao.java             # DAO 接口基类 (get, findList, insert, update, delete)
│   │   ├── Page.java                # 分页对象
│   │   └── annotation/MyBatisDao.java  # MyBatis DAO 标记注解
│   ├── service/
│   │   ├── BaseService.java         # Service 基类
│   │   └── CrudService.java         # CRUD Service 基类 (泛型 D extends CrudDao, T extends DataEntity)
│   ├── web/BaseController.java      # Controller 基类
│   └── utils/                       # 工具类 (StringUtils, DateUtils, IdGen 等)
├── modules/                         # 业务模块 (按功能分包)
│   └── {moduleName}/
│       ├── entity/                  # 实体类
│       ├── dao/                     # DAO 接口
│       ├── service/                 # Service 类
│       ├── web/                     # Controller 类
│       ├── enums/                   # 枚举类 (可选)
│       ├── utils/                   # 模块工具类 (可选)
│       └── thread/                  # 线程类 (可选)
└── ...

各层编码规范

1. Entity 层

文件位置: src/main/java/com/jeeplus/modules/{moduleName}/entity/{EntityName}.java

规范要点:

  • 普通业务实体继承 DataEntity<T>,含工作流的实体继承 ActEntity<T>
  • 必须提供两个构造函数:无参构造、(String id) 构造调用 super(id)
  • 所有字段为 private,提供 public getter/setter
  • 日期字段 getter 上加 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  • 需要导出的字段 getter 上加 @ExcelField(title="字段名", align=2, sort=序号)
  • 字段注释放在字段声明同一行末尾,用 // 注释
  • 必须声明 private static final long serialVersionUID = 1L;
  • 关联对象以完整对象引用(如 private WorkStaffBasicInfo basicInfo
  • 列表字段初始化用 Lists.newArrayList()(如 private List<WorkClientAttachment> workAttachments = Lists.newArrayList()
/**
 * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
 */
package com.jeeplus.modules.{moduleName}.entity;

// imports...

/**
 * {模块中文名}Entity
 * @author {作者}
 * @version {日期}
 */
public class {EntityName} extends DataEntity<{EntityName}> {
    
    private static final long serialVersionUID = 1L;
    private String field1;      // 字段注释
    private Date field2;        // 字段注释
    
    public {EntityName}() {
        super();
    }
    
    public {EntityName}(String id){
        super(id);
    }
    
    @ExcelField(title="字段名", align=2, sort=7)
    public String getField1() { return field1; }
    public void setField1(String field1) { this.field1 = field1; }
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @ExcelField(title="日期字段", align=2, sort=8)
    public Date getField2() { return field2; }
    public void setField2(Date field2) { this.field2 = field2; }
}

2. DAO 层

文件位置: src/main/java/com/jeeplus/modules/{moduleName}/dao/{EntityName}Dao.java

规范要点:

  • 接口必须加 @MyBatisDao 注解
  • 必须继承 CrudDao<T> 接口
  • 不使用 implements,只声明自定义方法
  • 自定义方法的参数复杂时用 @Param 注解
/**
 * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
 */
package com.jeeplus.modules.{moduleName}.dao;

import com.jeeplus.common.persistence.CrudDao;
import com.jeeplus.common.persistence.annotation.MyBatisDao;
import com.jeeplus.modules.{moduleName}.entity.{EntityName};

/**
 * {模块中文名}DAO接口
 * @author {作者}
 * @version {日期}
 */
@MyBatisDao
public interface {EntityName}Dao extends CrudDao<{EntityName}> {
    // 自定义查询方法
    public List<{EntityName}> getByXxx(@Param("param1")String param1);
}

3. MyBatis XML 映射

文件位置: src/main/resources/mappings/modules/{moduleName}/{EntityName}Dao.xml

规范要点:

  • namespace 必须等于 DAO 接口全限定名
  • 定义 <sql id="{entityName}Columns"><sql id="{entityName}Joins"> 片段
  • 列别名使用双引号(如 a.field AS "field"
  • 关联嵌套对象使用点分隔别名(如 w.name AS "basicInfo.name"
  • CRUD 标准方法:get, findList, findAllList, insert, update, delete, deleteByLogic, findUniqueByProperty
  • 分页排序使用 page.orderBy 动态 ORDER BY
  • 条件判断使用 <if test="... != null and ... != ''">
  • 多数据库兼容写法:<if test="dbName == 'mysql'"> / oracle / mssql
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jeeplus.modules.{moduleName}.dao.{EntityName}Dao">
    
    <sql id="{entityName}Columns">
        a.id AS "id",
        a.create_by AS "createBy.id",
        a.create_date AS "createDate",
        a.update_by AS "updateBy.id",
        a.update_date AS "updateDate",
        a.remarks AS "remarks",
        a.del_flag AS "delFlag",
        a.field1 AS "field1",
        a.field2 AS "field2"
    </sql>
    
    <sql id="{entityName}Joins">
        LEFT JOIN xxx_table x ON x.id = a.xxx_id
        LEFT JOIN sys_office o ON o.id = a.office_id
        LEFT JOIN sys_user u ON u.id = a.create_by
    </sql>
    
    <select id="get" resultType="{EntityName}">
        SELECT <include refid="{entityName}Columns"/>
        FROM {table_name} a
        <include refid="{entityName}Joins"/>
        WHERE a.id = #{id}
    </select>
    
    <select id="findList" resultType="{EntityName}">
        SELECT <include refid="{entityName}Columns"/>
        FROM {table_name} a
        <include refid="{entityName}Joins"/>
        <where>
            a.del_flag = #{DEL_FLAG_NORMAL}
            <if test="field1 != null and field1 != ''">
                AND a.field1 = #{field1}
            </if>
        </where>
        <choose>
            <when test="page !=null and page.orderBy != null and page.orderBy != ''">
                ORDER BY ${page.orderBy}
            </when>
            <otherwise>
                ORDER BY a.update_date DESC
            </otherwise>
        </choose>
    </select>
    
    <insert id="insert">
        INSERT INTO {table_name}(id, create_by, create_date, update_by, update_date, remarks, del_flag, field1, field2)
        VALUES (#{id}, #{createBy.id}, #{createDate}, #{updateBy.id}, #{updateDate}, #{remarks}, #{delFlag}, #{field1}, #{field2})
    </insert>
    
    <update id="update">
        UPDATE {table_name} SET 
            update_by = #{updateBy.id}, update_date = #{updateDate}, remarks = #{remarks},
            field1 = #{field1}, field2 = #{field2}
        WHERE id = #{id}
    </update>
    
    <update id="delete">
        DELETE FROM {table_name} WHERE id = #{id}
    </update>
    
    <update id="deleteByLogic">
        UPDATE {table_name} SET del_flag = #{DEL_FLAG_DELETE} WHERE id = #{id}
    </update>
    
    <select id="findUniqueByProperty" resultType="{EntityName}" statementType="STATEMENT">
        select * FROM {table_name} where ${propertyName} = '${value}'
    </select>
</mapper>

4. Service 层

文件位置: src/main/java/com/jeeplus/modules/{moduleName}/service/{EntityName}Service.java

规范要点:

  • 类上加 @Service@Transactional(readOnly = true)
  • 必须继承 CrudService<{EntityName}Dao, {EntityName}>
  • 写操作方法加 @Transactional(readOnly = false)
  • 注入 DAO 使用 @Autowired,也可直接使用父类注入的 dao 字段
  • 数据权限过滤使用 dataScopeFilter() 放入 sqlMap.dsf
  • 覆盖 save() 方法时必须调用 super.save(entity) 并处理子表
/**
 * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
 */
package com.jeeplus.modules.{moduleName}.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jeeplus.common.persistence.Page;
import com.jeeplus.common.service.CrudService;
import com.jeeplus.modules.{moduleName}.entity.{EntityName};
import com.jeeplus.modules.{moduleName}.dao.{EntityName}Dao;

/**
 * {模块中文名}Service
 * @author {作者}
 * @version {日期}
 */
@Service
@Transactional(readOnly = true)
public class {EntityName}Service extends CrudService<{EntityName}Dao, {EntityName}> {
    
    public {EntityName} get(String id) {
        return super.get(id);
    }
    
    public List<{EntityName}> findList({EntityName} entity) {
        return super.findList(entity);
    }
    
    public Page<{EntityName}> findPage(Page<{EntityName}> page, {EntityName} entity) {
        return super.findPage(page, entity);
    }
    
    @Transactional(readOnly = false)
    public void save({EntityName} entity) {
        super.save(entity);
    }
    
    @Transactional(readOnly = false)
    public void delete({EntityName} entity) {
        super.delete(entity);
    }
}

5. Controller 层

文件位置: src/main/java/com/jeeplus/modules/{moduleName}/web/{EntityName}Controller.java

规范要点:

  • @Controller + @RequestMapping(value = "${adminPath}/{moduleName}/{entityName}")
  • 必须继承 BaseController,不能继承其他 Controller
  • 使用 @ModelAttribute 方法根据 id 参数获取实体
  • 方法顺序:listformsavedeleteexport/import → 自定义
  • 列表方法需加 @RequiresPermissions("{moduleName}:{entityName}:list")
  • 返回视图路径格式:modules/{moduleName}/{entityName}List / Form
  • 保存成功后 redirect 回列表页,使用 Global.getAdminPath() 获取管理路径前缀
  • 使用 addMessage(redirectAttributes, "消息") 传递消息
  • 编辑保存前使用 MyBeanUtils.copyBeanNotNull2Bean(formObj, dbObj) 合并数据
/**
 * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
 */
package com.jeeplus.modules.{moduleName}.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.jeeplus.common.config.Global;
import com.jeeplus.common.persistence.Page;
import com.jeeplus.common.web.BaseController;
import com.jeeplus.common.utils.StringUtils;
import com.jeeplus.modules.{moduleName}.entity.{EntityName};
import com.jeeplus.modules.{moduleName}.service.{EntityName}Service;

/**
 * {模块中文名}Controller
 * @author {作者}
 * @version {日期}
 */
@Controller
@RequestMapping(value = "${adminPath}/{moduleName}/{entityName}")
public class {EntityName}Controller extends BaseController {

    @Autowired
    private {EntityName}Service {entityName}Service;
    @Autowired
    private HttpServletRequest request;

    @ModelAttribute
    public {EntityName} get(@RequestParam(required=false) String id) {
        {EntityName} entity = null;
        if (StringUtils.isNotBlank(id)){
            entity = {entityName}Service.get(id);
        }
        if (entity == null){
            entity = new {EntityName}();
        }
        return entity;
    }

    @RequiresPermissions("{moduleName}:{entityName}:list")
    @RequestMapping(value = {"list", ""})
    public String list({EntityName} entity, HttpServletRequest request, HttpServletResponse response, Model model) {
        Page<{EntityName}> page = {entityName}Service.findPage(new Page<{EntityName}>(request, response), entity);
        model.addAttribute("page", page);
        return "modules/{moduleName}/{entityName}List";
    }

    @RequestMapping(value = "form")
    public String form({EntityName} entity, Model model) {
        model.addAttribute("{entityName}", entity);
        return "modules/{moduleName}/{entityName}Form";
    }

    @RequestMapping(value = "save")
    public String save({EntityName} entity, Model model, RedirectAttributes redirectAttributes) throws Exception {
        if (!beanValidator(model, entity)){
            return form(entity, model);
        }
        if (!entity.getIsNewRecord()){
            {EntityName} t = {entityName}Service.get(entity.getId());
            MyBeanUtils.copyBeanNotNull2Bean(entity, t);
            {entityName}Service.save(t);
        } else {
            {entityName}Service.save(entity);
        }
        addMessage(redirectAttributes, "保存成功");
        return "redirect:" + Global.getAdminPath() + "/{moduleName}/{entityName}/?repage";
    }

    @RequestMapping(value = "delete")
    public String delete({EntityName} entity, RedirectAttributes redirectAttributes) {
        {entityName}Service.delete(entity);
        addMessage(redirectAttributes, "删除成功");
        return "redirect:" + Global.getAdminPath() + "/{moduleName}/{entityName}/?repage";
    }
}

6. 前端 JSP 页面

文件位置: src/main/webapp/webpage/modules/{moduleName}/{entityName}List.jsp / {entityName}Form.jsp / {entityName}View.jsp

规范要点:

  • 使用 <jsp:include> 和 Jeeplus 自定义标签库(taglib.jsp 引入)
  • 表单使用 <form:form> 标签,modelAttribute 绑定后端对象
  • ${ctx} 获取上下文路径(来自 jeeplus.propertiesadminPath
  • 列表页使用 Layui 的 oa-table 表格:<table class="oa-table layui-table" id="contentTable"></table>
  • 分页使用 <table:page page="${page}"></table:page> 标签
  • 排序使用 <table:sortColumn id="orderBy" name="orderBy" value="${page.orderBy}" callback="sortOrRefresh();"/>
  • 按钮样式类:nav-btn nav-btn-addnav-btn-refreshnav-btn-importnav-btn-export
  • 弹窗使用 openDialogAdd()openDialogEdit()openDialogView() 函数
  • 后端消息使用 <sys:message content="${message}"/>
  • 表单验证使用 jQuery Validate
  • 文件上传限制检查:单文件限制 50MB,总文件限制 500MB
  • 列表 JS 初始化方式:
$(document).ready(function() {
    // 表格列配置
    var cols = [
        {field:'no', title:'编号', sortable: true, width:100},
        {field:'name', title:'名称', sortable: true},
        {field:'createDate', title:'创建时间', sortable: true, width:150},
        {fixed:'right', title:'操作', toolbar:'#operating', width:180}
    ];
    var table = new TableInit("{entityName}", cols, '${ctx}/{moduleName}/{entityName}', 
        "searchForm", true, "contentTable", "operating");
    table.init();
});

关键 JavaScript 组件构造器:

  • TableInit(tableName, cols, url, formId, checkFlag, tableId, toolId) — 初始化表格
  • TableTreeInit(tableName, cols, url, formId, tableId, toolId, parentField) — 树形表格
  • DetailTableInit(detailName, fieldName, cols, url, tableId, toolId, obj) — 子表

Layui 日期控件: 使用 lay('.di').each(function(){ laydate.render({ elem: this, type: 'date', trigger: 'click' });})

全局常量

  • BaseEntity.DEL_FLAG_NORMAL = "0" / DEL_FLAG_DELETE = "1" / DEL_FLAG_AUDIT = "2"
  • Global.getAdminPath() — 管理端根路径(默认 /a
  • ${ctx} — JSP 中引用 adminPath
  • UserUtils.getUser() — 获取当前登录用户
  • UserUtils.getSelectCompany() — 获取当前选中公司
  • UserUtils.getSelectOffice() — 获取当前选中部门

文件命名规则

类型 命名模式 示例
Entity {EntityName}.java LeaveApply.java
DAO 接口 {EntityName}Dao.java LeaveApplyDao.java
DAO XML {EntityName}Dao.xml LeaveApplyDao.xml
Service {EntityName}Service.java LeaveApplyService.java
Controller {EntityName}Controller.java LeaveApplyController.java
列表页 {entityName}List.jsp leaveApplyList.jsp
表单页 {entityName}Form.jsp leaveApplyForm.jsp
查看页 {entityName}View.jsp leaveApplyView.jsp
子表页 {childName}List.jsp leaveDetailList.jsp

数据库表命名

  • 表名使用下划线小写:{module_name}leave_applyleave_detail
  • 关联外键命名:{ref_table}_idstaff_idleave_id
  • 必备列:id(varchar 64), create_by, create_date, update_by, update_date, remarks, del_flag

其他注意事项

  1. 文件头版权声明: 所有 Java/JSP 文件必须保留 JeePlus 版权声明
  2. 缩进: 使用 Tab 缩进(4 空格宽度)
  3. 日期格式: Java 端 yyyy-MM-dd HH:mm:ss,前端 yyyy-MM-dd(日期控件)
  4. 代码中字符串比较StringUtils.isNotBlank()/isBlank() 而非 != null && !""
  5. 权限注解: Shiro 的 @RequiresPermissions("module:entity:action")
  6. JSON 序列化忽略: 对不需要 JSON 序列化的字段使用 @JsonIgnore@JSONField(serialize = false)
  7. import 语句会包含 com.google.common.collect.Listscom.google.common.collect.Maps
  8. Controller 基类注入 HttpServletRequest request 用于获取当前请求
  9. Excel 导出使用 ExportExcel/ImportExcel 工具类
  10. 子表处理:在 Service 的 save 中先 super.save(entity) 再分别处理子表 CRUD