# 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`,含工作流的实体继承 `ActEntity` - 必须提供两个构造函数:无参构造、`(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 workAttachments = Lists.newArrayList()`) ```java /** * Copyright © 2015-2020 JeePlus 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` 接口 - 不使用 `implements`,只声明自定义方法 - 自定义方法的参数复杂时用 `@Param` 注解 ```java /** * Copyright © 2015-2020 JeePlus 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 接口全限定名 - 定义 `` 和 `` 片段 - 列别名使用双引号(如 `a.field AS "field"`) - 关联嵌套对象使用点分隔别名(如 `w.name AS "basicInfo.name"`) - CRUD 标准方法:`get`, `findList`, `findAllList`, `insert`, `update`, `delete`, `deleteByLogic`, `findUniqueByProperty` - 分页排序使用 `page.orderBy` 动态 ORDER BY - 条件判断使用 ` ` - 多数据库兼容写法:`` / `oracle` / `mssql` ```xml 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" 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 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}) UPDATE {table_name} SET update_by = #{updateBy.id}, update_date = #{updateDate}, remarks = #{remarks}, field1 = #{field1}, field2 = #{field2} WHERE id = #{id} DELETE FROM {table_name} WHERE id = #{id} UPDATE {table_name} SET del_flag = #{DEL_FLAG_DELETE} WHERE id = #{id} ``` ### 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)` 并处理子表 ```java /** * Copyright © 2015-2020 JeePlus 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` 参数获取实体 - 方法顺序:`list` → `form` → `save` → `delete` → `export/import` → 自定义 - 列表方法需加 `@RequiresPermissions("{moduleName}:{entityName}:list")` - 返回视图路径格式:`modules/{moduleName}/{entityName}List` / `Form` - 保存成功后 `redirect` 回列表页,使用 `Global.getAdminPath()` 获取管理路径前缀 - 使用 `addMessage(redirectAttributes, "消息")` 传递消息 - 编辑保存前使用 `MyBeanUtils.copyBeanNotNull2Bean(formObj, dbObj)` 合并数据 ```java /** * Copyright © 2015-2020 JeePlus 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` **规范要点**: - 使用 `` 和 Jeeplus 自定义标签库(`taglib.jsp` 引入) - 表单使用 `` 标签,`modelAttribute` 绑定后端对象 - `${ctx}` 获取上下文路径(来自 `jeeplus.properties` 的 `adminPath`) - 列表页使用 Layui 的 oa-table 表格:`
` - 分页使用 `` 标签 - 排序使用 `` - 按钮样式类:`nav-btn nav-btn-add`、`nav-btn-refresh`、`nav-btn-import`、`nav-btn-export` - 弹窗使用 `openDialogAdd()`、`openDialogEdit()`、`openDialogView()` 函数 - 后端消息使用 `` - 表单验证使用 jQuery Validate - 文件上传限制检查:单文件限制 50MB,总文件限制 500MB - 列表 JS 初始化方式: ```javascript $(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_apply`、`leave_detail` - 关联外键命名:`{ref_table}_id` 如 `staff_id`、`leave_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.Lists`、`com.google.common.collect.Maps` 8. **Controller 基类**注入 `HttpServletRequest request` 用于获取当前请求 9. **Excel 导出**使用 `ExportExcel`/`ImportExcel` 工具类 10. **子表处理**:在 Service 的 save 中先 `super.save(entity)` 再分别处理子表 CRUD