Instruction file imported from doramart/DoraCMS (
.cursor/rules/repository-modules.mdc). Copyright stays with the author.
🏗️ EggJS CMS 模块创建规则 (Cursor Rule) - 三层架构优化版
📋 项目架构概述
本项目采用 Repository/Adapter 模式,支持 MongoDB 和 MariaDB 双数据库,通过标准化参数接口实现数据库抽象。已完成 Admin、Role、Menu、ContentTag 模块重构,并建立了完善的三层基类架构。
🔥 Admin & Role & Menu 模块重构成果 + 2024架构优化 (实战验证版)
- ✅ MongoDB & MariaDB 双数据库接口已调试成功,两套接口完全兼容
- ✅ ID映射策略 已优化,MongoDB直接使用业务层id,MariaDB自动转换
- ✅ 关联关系处理 已完善,Admin-Role多对多关系、Menu树形结构完美支持
- ✅ JSON字段处理 已优化,Role.menus/buttons、Menu.query/buttons字段自动解析
- ✅ 字段自动检测 已实现,从Schema自动获取字段,零维护成本
- ✅ 树形结构处理 已完善,Menu模块支持平铺(flat=true)和树形(flat=false)输出
- ✅ 智能更新策略 已集成,MariaDB支持关联字段批量更新
- ✅ 业务特有方法 已完整迁移,登录验证、唯一性检查、统计分析等
- ✅ 基类通用方法 已提取,MongoDB/MariaDB Repository代码减少90%+
- ✅ 钩子方法体系 已建立,_customProcessDataItem、_getDefaultPopulate等
- ✅ 三层基类架构 已验证,BaseStandardRepository → BaseMongoRepository → 具体Repository
- ✅ Controller层标准化 已实现,filters: { status: { $eq: '1' } }、fields: ['name', 'title']
- ✅ 密码安全处理 已优化,默认排除密码字段,支持includePassword选项
- ✅ 事务处理机制 已完善,MariaDB关联操作支持事务回滚
- ✅ 性能优化策略 已实现,自动排除关联字段查询,避免N+1问题
- ✅ 统一异常处理 已完善,MongoDB/MariaDB异常处理逻辑完全一致
- ✅ 异常管理器 已集成,基类提供this.exceptions语义化异常创建
- ✅ 错误中间件 已部署,全局异常捕获和标准化响应
- ✅ MariaDB异常处理优化 已完成,与MongoDB版本保持100%一致的异常处理机制
🔥 ContentTag 模块重构成果 + 异常处理完善 (2024最新实战版)
- ✅ 三层架构重构:MongoDB/MariaDB Repository继承对应基类,代码减少70%+
- ✅ 异常处理体系:完整实现
this.exceptions.contentTag异常方法集 - ✅ 数据验证增强:创建/更新前自动验证字段长度、格式、必填等
- ✅ 唯一性约束:标签名称、别名唯一性检查,自动抛出语义化异常
- ✅ 批量操作优化:批量创建标签时检查重复,防止数据不一致
- ✅ Service层简化:移除try-catch块,异常自然抛出,代码减少27%
- ✅ 字段自动检测:MariaDB Schema字段自动获取,零维护成本
- ✅ 钩子方法完善:
_customPreprocessForCreate/Update集成数据验证 - ✅ 异常方法验证:通过测试脚本验证所有异常方法正常工作
- ✅ 统一异常格式:MongoDB/MariaDB异常处理100%一致,前端体验统一
🎯 ContentTag异常方法清单:
nameExists/aliasExists- 唯一性约束异常nameRequired/nameTooLong/aliasTooLong- 数据验证异常invalidAlias- 格式验证异常notFound/inUse- 资源和业务规则异常duplicateInBatch- 批量操作异常
💡 核心设计理念
简化原则:在MongoDB场景下,业务层 id 直接对应数据库层 _id,避免不必要的映射转换,只在返回完整对象给业务层时进行ID字段统一。
🚨 重要注意事项(基于实际重构经验)
1. 字段映射自动化优化 🔥
- ✅ 已优化:自动从Schema获取字段 - 不再需要手动维护字段列表
- ✅ 智能过滤机制 - 自动排除关联字段和虚拟字段,避免查询错误
- ✅ 灵活扩展点 - 通过
_getExcludeTableFields()和_getAdditionalTableFields()自定义 - ⚠️ 仍需注意:Schema字段名与业务代码使用保持一致(如
order不是sort)
2. 特殊业务逻辑识别 ⚠️
- 不能盲目删除看似重复的方法,可能包含重要业务逻辑
- 树形结构处理、关联关系处理等特殊逻辑需要保留
- 使用"继承基类 + 重写特殊逻辑"的模式
3. JSON 字段处理 ⚠️
- MariaDB 中的 JSON 字段需要正确的 get/set 方法
- 使用
_deepToJSON()确保关联对象正确序列化 - 避免
toJSON方法与get/set方法冲突
4. 关联关系处理 ⚠️
- 优先使用标准的数据库关联关系,避免 JSON 字段存储关联 ID
- MariaDB 使用
belongsToMany+ 中间表,MongoDB 使用populate - 确保
_ensureConnection()在关联查询前调用 - 实战经验:Admin-Role多对多关系,使用中间表AdminRole,支持状态控制和软删除
5. 密码安全处理 🔐 (新增)
- 默认查询自动排除密码字段:
options.fields = '-password' - 支持明确包含密码:
options.includePassword = true - 创建/更新时自动加密:
CryptoUtil.encryptPassword() - 登录验证专用字段选择:
fields: '_id userName password status'
6. 事务处理机制 💼
- MariaDB关联操作使用事务确保数据一致性
- 创建Admin时同步创建角色关联关系
- 异常时自动回滚,避免数据不一致
- MongoDB使用Session处理事务(可选)
7. 唯一性检查统一处理 🔧 (重要)
- 🔥 必须使用UniqueChecker: 所有唯一性验证都必须基于
UniqueChecker工具类统一处理 - 统一跨数据库兼容: UniqueChecker自动处理MongoDB(_id)和MariaDB(id)的字段差异
- 避免重复实现: 禁止在Repository中实现私有的
_checkFieldUnique方法 - 标准调用方式:
await UniqueChecker.checkTagNameUnique(this, name, excludeId) - 模块专用方法: 每个模块都有对应的专用检查方法(如
checkCategoryNameUnique,checkUserNameUnique等)
8. 统一异常处理机制 🚨 (2024最新优化)
- 异常管理器集成:基类提供
this.exceptions语义化异常创建 - MongoDB/MariaDB完全一致:两个适配器异常处理逻辑100%统一,前端体验完全相同
- 业务验证异常化:唯一性检查、登录验证等抛出具体异常
- 全局错误中间件:统一异常捕获和标准化响应
- Controller层简化:移除重复try-catch,专注业务逻辑
- 🔥 MariaDB特殊优化:支持多种根节点ID格式,JSON字段异常处理,事务异常回滚
- 跨数据库异常传递:业务异常透传,系统异常统一处理,确保异常堆栈清晰可追踪
9. 异常方法实现规范 🔥 (新增)
🎯 重要提醒:重构新模块时,如果Repository中使用了 this.exceptions.{模块名}.xxx() 方法,必须在 server/app/repository/base/RepositoryExceptions.js 中补充对应的异常方法。
9.1 异常方法补充流程
- 识别异常需求:分析模块业务逻辑,识别需要的异常类型
- 添加异常方法:在
RepositoryExceptions.js中添加对应的静态方法 - 验证异常功能:确保异常方法正常工作
- 更新Repository:在Repository中使用新的异常方法
9.2 异常方法分类和命名规范
🔥 按业务模块分类:每个模块都有独立的异常方法组
// 示例:ContentTag模块异常方法
static contentTag = {
// 唯一性约束异常
nameExists: value => ErrorFactory.uniqueConstraint('name', value, '标签名称已存在'),
aliasExists: value => ErrorFactory.uniqueConstraint('alias', value, '标签别名已存在'),
// 资源未找到异常
notFound: id => ErrorFactory.notFound('标签', id),
// 验证异常
nameRequired: () => ErrorFactory.validation('标签名称不能为空'),
nameTooLong: maxLength => ErrorFactory.validation(`标签名称长度不能超过${maxLength}个字符`),
invalidAlias: alias => ErrorFactory.validation(`标签别名 "${alias}" 格式不正确,只能包含字母、数字和连字符`),
// 业务规则异常
inUse: tagId => ErrorFactory.businessRule('TAG_IN_USE', `标签 ${tagId} 正在被使用,无法删除`),
duplicateInBatch: names => ErrorFactory.businessRule('DUPLICATE_IN_BATCH', `批量操作中存在重复的标签名称: ${names.join(', ')}`),
};
9.3 异常类型映射表
| 异常场景 | ErrorFactory方法 | 异常类 | 状态码 | 使用示例 |
|---|---|---|---|---|
| 唯一性约束 | uniqueConstraint() |
UniqueConstraintError | 400 | 用户名/邮箱已存在、标签名称重复等 |
| 资源未找到 | notFound() |
NotFoundError | 404 | 用户不存在、标签不存在、菜单不存在等 |
| 数据验证失败 | validation() |
ValidationError | 400 | 必填字段为空、格式不正确、长度超限等 |
| 业务规则违反 | businessRule() |
BusinessRuleError | 400 | 用户被禁用、资源正在使用、循环引用等 |
| 权限不足 | permission() |
PermissionError | 403 | 无权访问资源、无权执行操作等 |
| 认证失败 | authentication() |
AuthenticationError | 401 | 用户名密码错误、会话过期等 |
9.4 异常方法命名约定
🎯 命名规则:动词 + 名词 + 描述
// ✅ 推荐命名
nameExists; // 名称已存在
aliasExists; // 别名已存在
notFound; // 未找到
nameRequired; // 名称必填
nameTooLong; // 名称过长
invalidStatus; // 无效状态
inUse; // 正在使用
hasChildren; // 有子项目
circularReference; // 循环引用
// ❌ 避免的命名
checkName; // 太模糊
validateAlias; // 不是异常描述
errorName; // 冗余前缀
9.5 实际使用模式
🔥 在Repository中使用异常方法:
// 唯一性检查 - 统一异常处理版本
async checkNameUnique(name, excludeId = null) {
try {
const isUnique = await this._checkFieldUnique('name', name, excludeId);
if (!isUnique) {
throw this.exceptions.contentTag.nameExists(name); // 🔥 使用模块异常方法
}
return true;
} catch (error) {
if (error.name === 'UniqueConstraintError') {
throw error; // 透传业务异常
}
this._handleError(error, 'checkNameUnique', { name, excludeId });
}
}
// 数据验证 - 在钩子方法中使用
_customPreprocessForCreate(data) {
// 验证必填字段
if (!data.name || data.name.trim() === '') {
throw this.exceptions.contentTag.nameRequired(); // 🔥 使用验证异常方法
}
// 验证字段长度
if (data.name && data.name.length > 100) {
throw this.exceptions.contentTag.nameTooLong(100); // 🔥 使用长度验证方法
}
return data;
}
🔥 在Service层中使用:
async create(payload) {
// 验证唯一性 - 自动抛出异常,无需手动检查返回值
if (payload.name) {
await this.repository.checkNameUnique(payload.name);
}
if (payload.alias) {
await this.repository.checkAliasUnique(payload.alias);
}
return await this.repository.create(payload);
}
9.6 异常方法测试验证
🔥 必须验证异常方法是否正常工作:
// 创建临时测试文件验证异常方法
const RepositoryExceptions = require('./server/app/repository/base/RepositoryExceptions');
// 测试新添加的异常方法
console.log('测试 {模块名} 异常方法:');
const nameExistsError = RepositoryExceptions.{模块名}.nameExists('测试值');
console.log(`类型: ${nameExistsError.name}, 消息: ${nameExistsError.message}`);
🚀 快速验证命令:
# 创建临时测试文件
cat > test_exceptions.js << 'EOF'
const RepositoryExceptions = require('./server/app/repository/base/RepositoryExceptions');
console.log('测试异常方法:', RepositoryExceptions.{模块名}.nameExists('test'));
EOF
# 运行测试
node test_exceptions.js
# 清理测试文件
rm test_exceptions.js
9.7 常见模块异常方法模板
🎯 通用模块异常方法模板(可复制修改):
/**
* {模块名}相关异常
*/
static {模块名} = {
// 唯一性约束异常
nameExists: value => ErrorFactory.uniqueConstraint('name', value, '{模块中文名}名称已存在'),
codeExists: value => ErrorFactory.uniqueConstraint('code', value, '{模块中文名}代码已存在'),
// 资源未找到异常
notFound: id => ErrorFactory.notFound('{模块中文名}', id),
// 验证异常
nameRequired: () => ErrorFactory.validation('{模块中文名}名称不能为空'),
nameTooLong: maxLength => ErrorFactory.validation(`{模块中文名}名称长度不能超过${maxLength}个字符`),
invalidStatus: value => ErrorFactory.validation(`无效的{模块中文名}状态: ${value}`),
// 业务规则异常
inUse: id => ErrorFactory.businessRule('{MODULE}_IN_USE', `{模块中文名} ${id} 正在被使用,无法删除`),
};
🔥 重要提醒:
- 必须补充异常方法:Repository中使用的异常方法必须在
RepositoryExceptions.js中定义 - 必须使用UniqueChecker:所有唯一性验证都必须使用
UniqueChecker统一处理,禁止实现私有的_checkFieldUnique方法 - 保持命名一致性:异常方法命名要与业务场景匹配
- 验证异常功能:添加异常方法后必须验证其正常工作
- 文档同步更新:新增异常方法要在此规则文档中记录
🔧 UniqueChecker 统一唯一性检查规范 🔥 (重要)
📋 强制要求
所有涉及唯一性检查的Repository都必须基于 UniqueChecker 统一处理,这是确保代码一致性和跨数据库兼容性的关键。
🚫 禁止做法
// ❌ 禁止:实现私有的 _checkFieldUnique 方法
async _checkFieldUnique(field, value, excludeId = null) {
let query = { [field]: { $eq: value } };
if (excludeId) {
query.id = { $ne: excludeId };
}
const result = await this.findOne(query);
return !result;
}
// ❌ 禁止:直接在Repository中编写唯一性检查逻辑
async checkNameUnique(name, excludeId = null) {
const existing = await this.model.findOne({ name });
return !existing;
}
✅ 标准做法
// ✅ 正确:统一使用UniqueChecker
async checkNameUnique(name, excludeId = null) {
try {
// 🔥 使用对应模块的专用方法
const isUnique = await UniqueChecker.checkTagNameUnique(this, name, excludeId);
if (!isUnique) {
throw this.exceptions.contentTag.nameExists(name);
}
return true;
} catch (error) {
if (error.name === 'UniqueConstraintError') {
throw error;
}
this._handleError(error, 'checkNameUnique', { name, excludeId });
}
}
🎯 可用的UniqueChecker方法
| 模块 | 方法名 | 用途 |
|---|---|---|
| ContentCategory | checkCategoryNameUnique(repo, name, parentId, excludeId) |
分类名称唯一性(同级别) |
| ContentCategory | checkCategoryDefaultUrlUnique(repo, defaultUrl, excludeId) |
分类URL唯一性 |
| ContentTag | checkTagNameUnique(repo, name, excludeId) |
标签名称唯一性 |
| ContentTag | checkTagAliasUnique(repo, alias, excludeId) |
标签别名唯一性 |
| Admin | checkUserNameUnique(repo, userName, excludeId) |
用户名唯一性 |
| Admin | checkEmailUnique(repo, userEmail, excludeId) |
邮箱唯一性 |
| Admin | checkPhoneUnique(repo, userPhone, excludeId) |
手机号唯一性 |
| Role | checkRoleCodeUnique(repo, roleCode, excludeId) |
角色编码唯一性 |
| Menu | checkRoutePathUnique(repo, routePath, excludeId) |
路由路径唯一性 |
| Menu | checkRouteNameUnique(repo, routeName, excludeId) |
路由名称唯一性 |
🔧 跨数据库兼容性
UniqueChecker自动处理MongoDB和MariaDB的差异:
// UniqueChecker内部自动处理
if (repository.constructor.name.includes('Mongo')) {
query._id = { $ne: excludeId }; // MongoDB使用_id和$ne
} else {
const { Op } = require('sequelize');
query.id = { [Op.ne]: excludeId }; // MariaDB使用id和Op.ne
}
📊 优化收益
| 指标 | 使用前 | 使用后 | 改善幅度 |
|---|---|---|---|
| 代码一致性 | 60% | 100% | +67% |
| 维护成本 | 每模块独立维护 | 统一维护 | 降低80% |
| 跨数据库兼容 | 需手动处理 | 自动兼容 | 100%提升 |
| 重复代码 | 每模块20行 | 调用即用 | 减少95% |
🚀 扩展新模块
当需要为新模块添加唯一性检查时:
-
在UniqueChecker中添加专用方法:
// 在UniqueChecker.js中添加 static async checkNewModuleFieldUnique(repository, fieldValue, excludeId = null) { return await this.checkFieldUnique(repository, 'fieldName', fieldValue, excludeId); } -
在Repository中使用:
async checkFieldUnique(fieldValue, excludeId = null) { const isUnique = await UniqueChecker.checkNewModuleFieldUnique(this, fieldValue, excludeId); if (!isUnique) { throw this.exceptions.newModule.fieldExists(fieldValue); } return true; }
🎯 关键要点:
- 统一入口:所有唯一性检查都通过UniqueChecker
- 自动兼容:无需关心MongoDB/MariaDB差异
- 易于扩展:新增检查只需调用通用方法
- 维护简单:逻辑集中,一处修改全部受益
🔧 三层基类架构 (2024优化版)
IBaseRepository (接口层)
↓
BaseStandardRepository (跨数据库基础类)
↓ ↓
BaseMongoRepository BaseMariaRepository
(MongoDB专用基础类) (MariaDB专用基础类)
↓ ↓
MenuMongoRepository MenuMariaRepository
(具体业务实现) (具体业务实现)
🔑 关键架构特点 (2024实战验证版)
- 三层分离: 跨数据库基础类 → 数据库专用基础类 → 具体业务实现
- BaseStandardRepository: 纯跨数据库通用方法(
_standardizeParams、_processResult等) - BaseMongoRepository: MongoDB专用方法 +
general.js迁移的所有CRUD方法 - BaseMariaRepository: MariaDB专用方法 + Sequelize特定逻辑 + 深度JSON转换
- 具体Repository: 只包含业务特定方法,基础CRUD全部继承
- 钩子方法体系: 通过钩子方法实现灵活的业务扩展
- 代码复用: 重复代码减少90%+,维护成本大幅降低
- 智能字段自动检测: 从Sequelize模型自动获取字段,零维护成本
- 深度对象转换:
_deepToJSON确保 Sequelize 实例完全转换 - 特殊逻辑保护: 继承 + 重写模式,保留重要业务逻辑
🚀 新增实战验证的架构特点
- 密码安全机制: 自动排除密码字段,支持includePassword选项
- 关联关系优化: Admin-Role多对多、Menu树形结构完美支持
- 事务完整性: MariaDB关联操作事务保护,异常自动回滚
- JSON字段智能处理: Role.menus/buttons、Menu.query/buttons自动解析
- 性能优化策略: 自动排除关联字段查询,避免N+1问题
- 业务验证集成: UniqueChecker统一唯一性验证,CryptoUtil加密处理
- 错误处理统一: 异常管理器标准化异常,操作日志自动记录
- Controller标准化: filters操作符格式、fields数组格式统一
- 异常处理一致性: MongoDB/MariaDB异常处理逻辑完全统一
- 语义化异常: this.exceptions.user.nameExists()等直观的异常创建方法
🔧 核心设计原则
1. 分层架构
Controller (控制器层 - 参数标准化 + 业务验证)
↓
Service (服务层 - 薄代理层,直接调用Repository)
↓
Repository (数据访问层 - 核心逻辑 + 业务特有方法)
↓
Database Adapters (数据库适配器 - MongoDB/MariaDB)
↓
MongoDB / MariaDB
2. 统一参数接口
所有数据库操作必须使用标准化参数格式:
{
filters: { field: { $eq: value } }, // 查询条件,使用操作符格式
populate: [{ path: 'relation', select: [...] }], // 关联查询
sort: [{ field: 'fieldName', order: 'desc' }], // 排序配置
fields: ['field1', 'field2'], // 字段选择
pagination: { page: 1, pageSize: 10 } // 分页配置
}
3. 重构流程指南 ⭐
第一步:分析现有模块
- 检查 Schema 定义:确认数据库字段名和类型
- 识别特殊业务逻辑:找出不能简单继承的方法
- 分析关联关系:确定使用标准关联还是 JSON 字段
- 检查 JSON 字段:确认需要自定义 get/set 方法的字段
第二步:创建 Schema
// 🔥 关键:字段名必须与代码中使用的完全一致
order: { // ✅ 不是 sort
type: DataTypes.INTEGER,
defaultValue: 0,
},
hideInMenu: { // ✅ 不是 isShow
type: DataTypes.BOOLEAN,
defaultValue: false,
}
第三步:重构 Repository
// 1. 继承基类
class NewMariaRepository extends BaseMariaRepository {
// 2. 重写配置方法
_getDefaultSort() {
return [{ field: 'order', order: 'asc' }]; // ✅ 使用正确字段名
}
// 3. 重写有特殊逻辑的基类方法
async find(payload, options) {
const result = await super.find(payload, options);
// 添加特殊逻辑(如树形结构处理)
return this.processSpecialLogic(result, payload);
}
// 4. 保留业务特有方法
async getSpecialBusinessData() {
// 业务特有逻辑
}
}
第四步:验证和测试
- 字段自动检测测试:确认基类能正确从Schema获取字段
- 关联查询测试:验证 populate/include 功能
- 特殊逻辑测试:确保业务功能完整
- 性能测试:验证查询效率
4. ID映射策略 ⭐
MongoDB场景(简化策略)
// ✅ 输入:业务层id直接作为MongoDB的_id
const query = { _id: businessId };
// ✅ 输出:只在返回完整对象时转换
const processedMenus = allMenus.map(menu => this._mapIdFromDatabase(menu));
MariaDB场景(转换策略)
// 🔄 输入:业务层id转换为MariaDB的id
const mariadbId = this.transformer.transformQueryForMariaDB({ id: businessId }).id;
// 🔄 输出:MariaDB结果自动转换为业务层格式
return result.toJSON(); // 自动处理ID映射
📂 必需文件结构
基于 Menu模块 的标准化文件结构:
1. Repository 层文件
server/app/repository/
├── adapters/
│ ├── mongodb/
│ │ └── {ModuleName}MongoRepository.js # 继承BaseStandardRepository
│ └── mariadb/
│ └── {ModuleName}MariaRepository.js # 继承BaseMariaRepository
└── schemas/
└── mariadb/
└── {ModuleName}Schema.js # Sequelize模型定义
2. Service 层文件
server/app/service/
└── {moduleName}New.js # 薄代理层,调用Repository
3. Controller 层文件
server/app/controller/
├── apiNew/
│ └── {moduleName}.js # 前端API接口
└── manageNew/
└── {moduleName}.js # 管理后台接口
4. 模型文件(MongoDB)
server/app/model/
└── {moduleName}.js # MongoDB Schema定义
5. 验证文件
server/app/validate/
└── {moduleName}.js # 参数验证规则
🏗️ 文件创建模板
1. MongoDB Repository 模板 ⭐ (2024三层架构版)
文件路径: server/app/repository/adapters/mongodb/{ModuleName}MongoRepository.js
基于Menu & Role模块的最新优化实践:
/**
* 标准化的 {ModuleName} MongoDB Repository
* 基于三层架构优化版本 (2024)
* 🔥 继承BaseMongoRepository,基础CRUD方法全部自动获得
* 🎯 只需实现业务特定方法和钩子方法
*/
'use strict';
const BaseMongoRepository = require('../../base/BaseMongoRepository');
const UniqueChecker = require('../../../utils/UniqueChecker'); // 🔥 必须导入UniqueChecker
const _ = require('lodash');
class {ModuleName}MongoRepository extends BaseMongoRepository {
constructor(ctx) {
super(ctx, '{ModuleName}');
// 设置 MongoDB 模型
this.model = this.app.model.{ModuleName};
// 注册模型和关联关系
this.registerModel({
mongoModel: this.model,
relations: {
// 定义关联关系,例如:
// author: {
// model: this.app.model.User,
// path: 'author',
// select: ['userName', 'nickName', 'logo'],
// },
},
});
}
/**
* 获取默认的关联查询配置
* @return {Array} 默认的 populate 配置
* @protected
*/
_getDefaultPopulate() {
return [
// 根据需要添加默认的关联查询
// { path: 'author', select: ['userName', 'nickName', 'logo'] },
];
}
/**
* 获取默认的搜索字段
* @return {Array} 默认的搜索字段
* @protected
*/
_getDefaultSearchKeys() {
return ['name', 'title', 'description']; // 根据模块调整
}
/**
* 获取默认的排序配置
* @return {Array} 默认的排序配置
* @protected
*/
_getDefaultSort() {
return [
{ field: 'order', order: 'asc' }, // 通用排序字段
{ field: 'createdAt', order: 'desc' }, // 创建时间
];
}
/**
* 🔥 重写查找方法(仅在需要特殊处理时)
* 实战案例:Admin模块排除密码字段、Menu模块树形结构处理
* 注意:如无特殊需求,直接继承基类方法即可
* @param {Object} payload 查询参数
* @param {Object} options 查询选项
* @return {Promise<Object|Array>} 查询结果
*/
async find(payload = {}, options = {}) {
// 🔥 Admin模块实战经验:默认排除密码字段
// if (!options.fields && !payload.fields) {
// options.fields = '-password';
// }
// 调用基类方法
const result = await super.find(payload, options);
// 🔥 Menu模块实战经验:树形结构处理
// if (!payload.flat && result && result.docs) {
// result.docs = this.buildTree(result.docs);
// } else if (!payload.flat && Array.isArray(result)) {
// return this.buildTree(result);
// }
return result;
}
/**
* 🔥 重写findOne方法(仅在需要特殊处理时)
* 实战案例:Admin模块密码字段处理
* @param {Object} params 查询参数
* @param {Object} options 查询选项
* @return {Promise<Object|null>} 查询结果
*/
async findOne(params = {}, options = {}) {
// 🔥 Admin模块实战经验:排除密码字段,除非明确要求包含
// if (!options.fields && !options.includePassword) {
// options.fields = '-password';
// }
// 调用基类方法
return await super.findOne(params, options);
}
/**
* 🔥 基类已实现的完整find方法(仅供参考,请勿重复实现)
* 子类通常不需要重写此方法,除非有特殊业务逻辑
*/
// async findWithFullImplementation(payload = {}, options = {}) {
// try {
// // 标准化参数
// const standardParams = this._standardizeParams(payload, options);
//
// // 构建搜索条件
// const searchCondition = this._buildSearchCondition(
// payload.searchkey,
// options.searchKeys || this._getDefaultSearchKeys()
// );
//
// // 构建基础查询条件
// let baseQuery = standardParams.query || {};
//
// // 合并查询条件
// const finalQuery = this._mergeQueryConditions(baseQuery, searchCondition);
//
// // 设置默认值
// const populate =
// standardParams.populate.length > 0 ? standardParams.populate : this._getDefaultPopulate();
//
// const sort =
// Object.keys(standardParams.sort).length > 0
// ? standardParams.sort
// : this._transformSortToMongo(this._getDefaultSort());
//
// // 执行查询
// const result = await _list(this.model, payload, {
// query: finalQuery,
// searchKeys: options.searchKeys || this._getDefaultSearchKeys(),
// populate,
// files: standardParams.fields,
// sort,
// });
//
// // 处理结果
// const processedResult = this._processResult(result, payload);
//
// // 记录操作日志
// this._logOperation('find', { payload, options }, processedResult);
//
// return processedResult;
// } catch (error) {
// this._handleError(error, 'find', { payload, options });
// }
// }
/**
* 将标准排序格式转换为 MongoDB 格式
* @param {Array} sortArray 标准排序数组
* @return {Object} MongoDB 排序对象
* @private
*/
_transformSortToMongo(sortArray) {
const mongoSort = {};
sortArray.forEach(config => {
mongoSort[config.field] = config.order === 'asc' ? 1 : -1;
});
return mongoSort;
}
/**
* 查找单条记录
* @param {Object} params 查询参数
* @param {Object} options 查询选项
* @return {Promise<Object|null>} 查询结果
*/
async findOne(params = {}, options = {}) {
try {
// 标准化参数
const standardParams = this._standardizeParams({}, { ...options, ...params });
// 设置默认值
const populate =
standardParams.populate.length > 0 ? standardParams.populate : this._getDefaultPopulate();
// 执行查询
const result = await this.model
.findOne(standardParams.query)
.populate(populate)
.select(standardParams.fields)
.lean();
if (!result) {
return null;
}
// 处理结果
const processedResult = this._postprocessData(result);
// 记录操作日志
this._logOperation('findOne', params, processedResult);
return processedResult;
} catch (error) {
this._handleError(error, 'findOne', params);
}
}
/**
* 根据ID查找记录
* @param {String} id 记录ID
* @param {Object} options 查询选项
* @return {Promise<Object|null>} 查询结果
*/
async findById(id, options = {}) {
try {
// 转换 id 参数
const query = { _id: id };
return await this.findOne(query, options);
} catch (error) {
this._handleError(error, 'findById', { id, options });
}
}
/**
* 统计记录数量
* @param {Object} query 查询条件
* @return {Promise<Number>} 记录数量
*/
async count(query = {}) {
try {
// 标准化查询条件
const standardParams = this._standardizeParams({}, { filters: query });
// 执行统计
const result = await _count(this.model, standardParams.query);
// 记录操作日志
this._logOperation('count', query, result);
return result;
} catch (error) {
this._handleError(error, 'count', query);
}
}
/**
* 创建记录
* @param {Object} data 要创建的数据
* @return {Promise<Object>} 创建的记录
*/
async create(data) {
try {
// 验证数据
const validation = this._validateData(data, 'create');
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// 预处理数据
const processedData = this._preprocessDataForCreate(data);
// 执行创建
const result = await _create(this.model, processedData);
// 后处理数据
const finalResult = this._postprocessData(result);
// 记录操作日志
this._logOperation('create', data, finalResult);
return finalResult;
} catch (error) {
this._handleError(error, 'create', data);
}
}
/**
* 更新记录
* @param {String} id 记录ID
* @param {Object} data 要更新的数据
* @return {Promise<Object>} 更新后的记录
*/
async update(id, data) {
try {
// 验证数据
const validation = this._validateData(data, 'update');
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// 预处理数据
const processedData = this._preprocessDataForUpdate(data);
// 执行更新
const result = await _update(this.ctx, this.model, id, processedData);
// 后处理数据
const finalResult = this._postprocessData(result);
// 记录操作日志
this._logOperation('update', { id, data }, finalResult);
return finalResult;
} catch (error) {
this._handleError(error, 'update', { id, data });
}
}
/**
* 删除记录
* @param {String|Array} ids 要删除的记录ID或ID数组
* @param {String} key 主键字段名,默认为 '_id'
* @return {Promise<Object>} 删除结果
*/
async remove(ids, key = '_id') {
try {
// 执行删除
const result = await _removes(this.ctx, this.model, ids, key);
// 记录操作日志
this._logOperation('remove', { ids, key }, result);
return result;
} catch (error) {
this._handleError(error, 'remove', { ids, key });
}
}
/**
* 软删除记录(标记删除)
* @param {String|Array} ids 要删除的记录ID或ID数组
* @param {Object} updateObj 更新对象,默认 { status: '0' }
* @return {Promise<Object>} 删除结果
*/
async safeDelete(ids, updateObj = { status: '0' }) {
try {
const idArray = Array.isArray(ids) ? ids : [ids];
// 🔥 关键:业务层传入的ids直接作为MongoDB的_id使用(基于Menu模块实践)
const result = await this.model.updateMany({ _id: { $in: idArray } }, { $set: updateObj });
// 记录操作日志
this._logOperation('safeDelete', { ids, updateObj }, result);
return result;
} catch (error) {
this._handleError(error, 'safeDelete', { ids, updateObj });
}
}
// ===== 🔥 业务特有方法实战模板(基于Admin/Menu/Role模块经验) =====
/**
* 🔥 统一异常处理版本:检查字段唯一性 - 必须使用UniqueChecker
* @param {String} userName 用户名
* @param {String} excludeId 排除的ID(用于更新时检查)
* @return {Promise<Boolean>} 是否唯一
* @throws {UniqueConstraintError} 当用户名已存在时抛出异常
*/
// async checkUserNameUnique(userName, excludeId = null) {
// try {
// // 🔥 必须使用UniqueChecker统一处理唯一性验证
// const isUnique = await UniqueChecker.checkUserNameUnique(this, userName, excludeId);
// if (!isUnique) {
// throw this.exceptions.user.nameExists(userName);
// }
// return true;
// } catch (error) {
// if (error.name === 'UniqueConstraintError') {
// throw error;
// }
// this._handleError(error, 'checkUserNameUnique', { userName, excludeId });
// }
// }
/**
* 🔥 Admin模块实战:登录验证 - 统一异常处理版本
* @param {String} identifier 登录标识(用户名、邮箱或手机号)
* @param {String} password 密码
* @param {String} loginType 登录类型
* @return {Promise<Object>} 用户信息
* @throws {AuthenticationError} 认证失败时抛出异常
* @throws {NotFoundError} 用户不存在时抛出异常
* @throws {BusinessRuleError} 用户被禁用时抛出异常
*/
// async verifyLogin(identifier, password, loginType) {
// try {
// let admin = null;
// switch (loginType) {
// case 'username':
// admin = await this.findByUserName(identifier, {
// fields: 'id userName password status nickName userRoles',
// includePassword: true,
// });
// break;
// case 'email':
// admin = await this.findByEmail(identifier, {
// fields: 'id userName password status nickName userRoles',
// includePassword: true,
// });
// break;
// case 'phone':
// admin = await this.findByPhone(identifier, {
// fields: 'id userName password status nickName userRoles',
// includePassword: true,
// });
// break;
// default:
// throw new Error('Invalid login type');
// }
//
// if (!admin) {
// throw this.exceptions.user.notFound();
// }
//
// if (admin.status !== SYSTEM_CONSTANTS.STATUS.ENABLED) {
// throw this.exceptions.user.disabled();
// }
//
// // 验证密码
// const isValidPassword = CryptoUtil.verifyPassword(
// password,
// admin.password,
// this.app.config.encrypt_key
// );
// if (!isValidPassword) {
// throw this.exceptions.user.invalidCredentials();
// }
//
// // 返回管理员信息(不包含密码)
// delete admin.password;
// return admin;
// } catch (error) {
// this._handleError(error, 'verifyLogin', { identifier, loginType });
// }
// }
/**
* 🔥 Menu模块实战:根据父ID查找子菜单
* @param {String} parentId 父级ID
* @param {Object} options 查询选项
* @return {Promise<Array>} 子菜单列表
*/
// async findByParentId(parentId = '0', options = {}) {
// const filters = { parentId, ...options.filters };
// return await this.find({}, { ...options, filters });
// }
/**
* 🔥 Menu模块实战:构建树形结构
* @param {Array} dataList 平铺数据列表
* @param {String} parentId 父级ID
* @return {Array} 树形结构数据
*/
// async buildTree(dataList, parentId = '0') {
// const tree = [];
// const rootParentIds = ['0', 0, null, undefined];
//
// dataList.forEach(item => {
// if (rootParentIds.includes(item.parentId) && rootParentIds.includes(parentId)) {
// // 根级节点
// const children = this.buildTree(dataList, item._id || item.id);
// tree.push({ ...item, children });
// } else if (String(item.parentId) === String(parentId)) {
// // 子节点
// const children = this.buildTree(dataList, item._id || item.id);
// tree.push({ ...item, children });
// }
// });
//
// return tree;
// }
/**
* 🔥 通用实战:批量更新状态
* @param {Array} ids ID数组
* @param {String} status 新状态
* @return {Promise<Object>} 更新结果
*/
// async batchUpdateStatus(ids, status) {
// try {
// const idArray = Array.isArray(ids) ? ids : [ids];
// const result = await this.model.updateMany(
// { _id: { $in: idArray } },
// { $set: { status, updatedAt: new Date() } }
// );
// return { modifiedCount: result.modifiedCount };
// } catch (error) {
// this._handleError(error, 'batchUpdateStatus', { ids, status });
// }
// }
/**
* 🔥 Admin模块实战:获取统计信息
* @param {Object} filter 过滤条件
* @return {Promise<Object>} 统计信息
*/
// async getEntityStats(filter = {}) {
// try {
// const pipeline = [
// { $match: filter },
// {
// $group: {
// _id: '$status',
// count: { $sum: 1 },
// },
// },
// ];
// const result = await this.model.aggregate(pipeline);
// const stats = { total: 0, enabled: 0, disabled: 0 };
// result.forEach(item => {
// const count = item.count;
// stats.total += count;
// switch (item._id) {
// case SYSTEM_CONSTANTS.STATUS.ENABLED:
// stats.enabled = count;
// break;
// case SYSTEM_CONSTANTS.STATUS.DISABLED:
// stats.disabled = count;
// break;
// }
// });
// return stats;
// } catch (error) {
// this._handleError(error, 'getEntityStats', { filter });
// return { total: 0, enabled: 0, disabled: 0 };
// }
// }
// ===== 🔥 基类钩子方法重写 - 用于业务特定逻辑 =====
/**
* 重写状态映射(可选,如果状态映射不同)
* @return {Object} 状态映射对象
* @protected
*/
_getStatusMapping() {
return SYSTEM_CONSTANTS.STATUS_TEXT; // 使用系统常量
// 或自定义映射:
// return {
// 0: '禁用',
// 1: '启用',
// };
}
/**
* 子类自定义的数据项处理(业务特定逻辑)
* @param {Object} item 预处理后的数据项
* @return {Object} 最终数据项
* @protected
*/
_customProcessDataItem(item) {
if (!item) return item;
// 🔥 添加业务特定的数据处理
// 例如:菜单类型文本、标签计数、特殊字段格式化等
// if (item.menuType) {
// item.menuTypeText = this._getMenuTypeText(item.menuType);
// }
// if (item.tags) {
// item.tagCount = item.tags.length;
// }
// 确保数组字段的默认值
// item.children = item.children || [];
// item.permissions = item.permissions || [];
return this.transformer.transformIdFields(item, 'fromDatabase');
}
/**
* 子类自定义的创建前数据处理(业务特定逻辑)
* @param {Object} data 预处理后的数据
* @return {Object} 最终数据
* @protected
*/
_customPreprocessForCreate(data) {
// 🔥 添加业务特定的创建前处理
// 例如:设置特殊默认值、计算字段、数据验证等
// if (!data.parentId) {
// data.parentId = '0';
// }
// if (!data.order) {
// data.order = 0;
// }
return data;
}
/**
* 子类自定义的更新前数据处理(业务特定逻辑)
* @param {Object} data 预处理后的数据
* @return {Object} 最终数据
* @protected
*/
_customPreprocessForUpdate(data) {
// 🔥 添加业务特定的更新前处理
// 例如:特殊字段处理、计算字段更新等
return data;
}
// 根据具体业务需求添加其他方法...
}
module.exports = {ModuleName}MongoRepository;
🔥 2024 MongoDB Repository 重要优化说明
基于三层架构优化后,MongoDB Repository的实现方式发生了重大变化:
✅ 基类已实现的方法(无需重复实现)
find()- 列表查询(除非需要特殊处理如树形结构)findOne()- 单条查询findById()- ID查询count()- 计数查询create()- 创建数据update()- 更新数据remove()- 删除数据safeDelete()- 软删除_transformSortToMongo()- 排序转换_postprocessData()- 数据后处理_processDataItem()- 数据项处理_getStatusText()- 状态文本获取_preprocessDataForCreate()- 创建前预处理_preprocessDataForUpdate()- 更新前预处理_standardizeParams()- 参数标准化_processResult()- 结果处理_handleError()- 错误处理
🎯 子类需要实现的内容
-
必须实现的钩子方法:
_getDefaultPopulate()- 默认关联查询_getDefaultSearchKeys()- 默认搜索字段_getDefaultSort()- 默认排序
-
可选实现的钩子方法:
_getStatusMapping()- 状态映射(如果不同于默认)_customProcessDataItem()- 自定义数据项处理_customPreprocessForCreate()- 自定义创建前处理_customPreprocessForUpdate()- 自定义更新前处理
-
业务特有方法:
- 如
checkUniqueField(),findByParentId(),buildTree()等
- 如
-
特殊重写方法:
- 如
find()方法(当需要树形结构处理时)
- 如
📋 简化后的模板结构
class {ModuleName}MongoRepository extends BaseMongoRepository {
constructor(ctx) { /* 基础配置 */ }
// 必须实现的钩子方法
_getDefaultPopulate() { /* 关联查询配置 */ }
_getDefaultSearchKeys() { /* 搜索字段 */ }
_getDefaultSort() { /* 排序配置 */ }
// 可选的钩子方法
_getStatusMapping() { /* 状态映射 */ }
_customProcessDataItem(item) { /* 数据处理 */ }
// 特殊重写(仅在需要时)
// async find(payload, options) { /* 调用super.find()后特殊处理 */ }
// 业务特有方法
// async checkUniqueField() { /* 业务逻辑 */ }
}
🚫 不要重复实现的方法 (2024版)
以下方法已在 BaseStandardRepository 或 BaseMongoRepository 中实现,子类不应重复实现:
BaseStandardRepository 提供的跨数据库方法:
_standardizeParams()- 参数标准化_processResult()- 结果处理_validateData()- 数据验证_logOperation()- 操作日志_handleError()- 错误处理transformer.formatTimeFields()- 时间字段格式化
BaseMongoRepository 提供的MongoDB专用方法:
find()- 列表查询(除非需要特殊处理)findOne()- 单条查询findById()- ID查询count()- 计数查询create()- 创建数据update()- 更新数据remove()- 删除数据safeDelete()- 软删除_transformSortToMongo()- 排序转换_postprocessData()- 数据后处理_processDataItem()- 数据项处理_getStatusText()- 状态文本获取_preprocessDataForCreate()- 创建前预处理_preprocessDataForUpdate()- 更新前预处理_buildSearchCondition()- 搜索条件构建_mergeQueryConditions()- 查询条件合并- 所有
_mongo*()内部方法(来自general.js)
⚠️ 重要提醒
- 不要导入
general.js- 所有方法已迁移到BaseMongoRepository - 不要重复实现基础CRUD - 直接继承使用
- 只实现业务特有逻辑 - 通过钩子方法扩展
- 默认配置方法:
_getDefaultPopulate(),_getDefaultSearchKeys(),_getDefaultSort() - 钩子方法重写:
_customProcessDataItem(),_customPreprocessForCreate(),_customPreprocessForUpdate() - 业务特有方法:
checkUniqueField(),findByParentId(),buildTree()等 - 状态映射重写:
_getStatusMapping()(如果状态映射不同)
📊 优化效果
- 代码减少:每个Repository减少~150行重复代码
- 维护性:通用逻辑统一管理,一处修改全部受益
- 一致性:所有Repository行为完全统一
- 扩展性:钩子方法提供灵活的业务定制点
2. MariaDB Repository 模板 ⭐
文件路径: server/app/repository/adapters/mariadb/{ModuleName}MariaRepository.js
基于Menu模块的成功实践:
/**
* 优化后的 {ModuleName} MariaDB Repository
* 🔥 基于增强的 BaseMariaRepository,专注于 {ModuleName} 特有的业务逻辑
* ✅ 继承基类的通用 CRUD 方法和 deepToJSON 处理
* ✅ 只需实现 {ModuleName} 特定的业务方法和配置
*
* 架构优化亮点:
* 1. 移除重复的基类 CRUD 方法 - 直接继承使用
* 2. 专注于 {ModuleName} 特有的业务逻辑
* 3. 可配置的钩子方法 - 灵活的数据处理管道
* 4. 统一的深度 JSON 转换 - 支持关联字段和 JSON 字段
*/
'use strict';
const BaseMariaRepository = require('../../base/BaseMariaRepository');
const MariaDBConnection = require('../../connections/MariaDBConnection');
const {ModuleName}Schema = require('../../schemas/mariadb/{ModuleName}Schema');
const UniqueChecker = require('../../../utils/UniqueChecker'); // 🔥 必须导入UniqueChecker
// 如果有关联关系,导入相关Schema
// const RelatedSchema = require('../../schemas/mariadb/RelatedSchema');
class {ModuleName}MariaRepository extends BaseMariaRepository {
constructor(ctx) {
super(ctx, '{ModuleName}');
// 初始化 MariaDB 连接
this.connection = new MariaDBConnection(ctx.app);
this.model = null;
// 如果有关联关系,初始化相关模型
// this.relatedModel = null;
// 注意:不在构造函数中同步调用 _initializeConnection
// 而是在 _ensureConnection 中异步调用
}
/**
* 初始化数据库连接和模型
* @private
*/
async _initializeConnection() {
try {
// 确保连接管理器已初始化
await this.connection.initialize();
const sequelize = this.connection.getSequelize();
// 直接创建模型实例,避免依赖连接管理器的缓存
this.model = {ModuleName}Schema(sequelize, this.app);
// 如果有关联关系,创建相关模型
// this.relatedModel = RelatedSchema(sequelize, this.app);
// 🔥 如果有关联关系,设置关联(参考Admin模块)
// this._setupAssociations();
// 注册模型和标准关联关系
this.registerModel({
mariaModel: this.model,
relations: {
// 标准关联关系会自动处理,这里只需要定义特殊的关联
},
});
console.log('✅ {ModuleName}MariaRepository initialized successfully');
} catch (error) {
console.error('❌ {ModuleName}MariaRepository initialization failed:', error);
throw error;
}
}
/**
* 确保连接已建立
* @private
*/
async _ensureConnection() {
if (!this.model) {
await this._initializeConnection();
}
}
// ===== 🔥 重写基类的抽象方法 - {ModuleName} 特有配置 =====
/**
* 获取默认的关联查询配置
* @return {Array} 默认的 populate 配置
* @protected
*/
_getDefaultPopulate() {
return []; // 根据业务需求配置关联查询
}
/**
* 获取默认的搜索字段
* @return {Array} 默认的搜索字段
* @protected
*/
_getDefaultSearchKeys() {
return ['name', 'title', 'description']; // 根据实际字段调整
}
/**
* 获取默认的排序配置
* @return {Array} 默认的排序配置
* @protected
*/
_getDefaultSort() {
return [
{ field: 'order', order: 'asc' }, // 🔥 注意:确保字段名与Schema一致
{ field: 'createdAt', order: 'desc' }
];
}
/**
* 获取状态映射
* @return {Object} 状态映射对象
* @protected
*/
_getStatusMapping() {
return {
1: '启用',
2: '禁用',
};
}
/**
* 🔥 优化版:不再需要手动维护字段列表!
* 基类会自动从Schema获取所有字段,大幅减少维护成本
* @return {Array} 有效字段列表
* @protected
*/
_getValidTableFields() {
// 直接使用基类的自动检测功能
return super._getValidTableFields();
}
/**
* 重写:获取需要排除的字段
* 🔥 只需要定义需要排除的关联字段和虚拟字段
* @return {Array} 排除字段列表
* @protected
*/
_getExcludeTableFields() {
const baseExcludeFields = super._getExcludeTableFields();
// {ModuleName}模块特有的需要排除的字段
const moduleExcludeFields = [
'relationField', // 关联字段 - 通过中间表或关联查询管理
'virtualField', // 虚拟字段 - 计算得出的字段
// 根据具体业务添加其他需要排除的字段
];
return [...baseExcludeFields, ...moduleExcludeFields];
}
/**
* 重写:获取额外需要包含的字段(可选)
* 🔥 如果有计算字段或特殊字段需要包含在查询中
* @return {Array} 额外字段列表
* @protected
*/
_getAdditionalTableFields() {
// 如果有一些计算字段或者特殊字段需要包含,在这里添加
return [
// 例如:'fullName', 'displayName' 等虚拟字段
];
}
/**
* 子类自定义的数据项处理 - {ModuleName} 特有逻辑
* @param {Object} item 预处理后的数据项
* @return {Object} 最终数据项
* @protected
*/
_customProcessDataItem(item) {
if (!item) return item;
// 调用基类方法添加状态文本
item = super._customProcessDataItem(item);
// 添加 {ModuleName} 特有的数据处理
// 例如:添加自定义字段、格式化特殊数据等
return item;
}
/**
* 子类自定义的创建前数据处理 - {ModuleName} 特有逻辑
* @param {Object} data 预处理后的数据
* @return {Object} 最终数据
* @protected
*/
_customPreprocessForCreate(data) {
// 调用基类方法
data = super._customPreprocessForCreate(data);
// 添加 {ModuleName} 特有的创建前处理
// 设置默认值
if (!data.status) data.status = '1'; // 默认启用
if (!data.order) data.order = 0; // 默认排序
return data;
}
/**
* 子类自定义的更新前数据处理 - {ModuleName} 特有逻辑
* @param {Object} data 预处理后的数据
* @return {Object} 最终数据
* @protected
*/
_customPreprocessForUpdate(data) {
// 调用基类方法
data = super._customPreprocessForUpdate(data);
// 添加 {ModuleName} 特有的更新前处理
return data;
}
// ===== 🔥 重写基类方法 - 添加 {ModuleName} 特殊逻辑 =====
/**
* 🔥 重写基类方法(仅在需要特殊处理时)
* 实战案例:Menu模块树形结构处理、Admin模块关联查询优化
* @param {Object} payload 查询参数
* @param {Object} options 选项
* @return {Promise} 查询结果
*/
// async find(payload = {}, options = {}) {
// await this._ensureConnection();
//
// try {
// // 调用基类的 find 方法
// const result = await super.find(payload, options);
//
// // 🔥 Menu模块实战经验:树形结构处理
// // if (!payload.flat && result && result.docs) {
// // result.docs = this.buildTree(result.docs);
// // } else if (!payload.flat && Array.isArray(result)) {
// // return this.buildTree(result);
// // }
//
// return result;
// } catch (error) {
// this._handleError(error, 'find', { payload, options });
// }
// }
/**
* 🔥 重写create方法以处理关联关系(参考Admin模块实战经验)
* @param {Object} data 创建数据
* @return {Promise<Object>} 创建结果
*/
// async create(data) {
// await this._ensureConnection();
// const transaction = await this.connection.getSequelize().transaction();
//
// try {
// // 分离关联字段和主数据
// const { userRoles, ...mainData } = data;
// const processedData = this._customPreprocessForCreate(mainData);
//
// // 创建主记录
// const result = await this.model.create(processedData, { transaction });
//
// // 处理关联关系(如Admin-Role关联)
// if (userRoles && Array.isArray(userRoles) && userRoles.length > 0) {
// await this._createRelations(result.id, userRoles, {
// transaction,
// createBy: data.createBy || 'system',
// });
// }
//
// await transaction.commit();
//
// // 获取完整数据(包含关联关系)
// const fullResult = await this.findById(result.id, {
// populate: this._getDefaultPopulate(),
// });
//
// this._logOperation('create', { data }, fullResult);
// return fullResult;
// } catch (error) {
// await transaction.rollback();
// this._handleError(error, 'create', { data });
// }
// }
// ===== 🔥 {ModuleName} 特有的业务方法 =====
/**
* 🔥 Menu模块实战:检查菜单是否可以删除(无子菜单)- 统一异常处理版本
* @param {String} menuId 菜单ID
* @return {Promise<Boolean>} 是否可以删除
* @throws {BusinessRuleError} 当菜单有子菜单时抛出异常
*/
// async checkMenuCanDelete(menuId) {
// try {
// const children = await this.findByParentId(menuId);
// if (children && children.length > 0) {
// throw this.exceptions.menu.hasChildren(menuId);
// }
// return true;
// } catch (error) {
// if (error.name === 'BusinessRuleError') {
// throw error;
// }
// this._handleError(error, 'checkMenuCanDelete', { menuId });
// }
// }
/**
* 🔥 Menu模块实战:检查父菜单是否存在 - 统一异常处理版本
* @param {String} parentId 父菜单ID
* @return {Promise<Boolean>} 父菜单是否存在
* @throws {NotFoundError} 当父菜单不存在时抛出异常
*/
// async checkParentMenuExists(parentId) {
// try {
// // 根级菜单无需检查(MariaDB支持多种根节点ID格式)
// if (
// parentId === SYSTEM_CONSTANTS.PERMISSION.ROOT_PARENT_ID ||
// parentId === SYSTEM_CONSTANTS.PERMISSION.ROOT_PARENT_ID_NUMBER
// !parentId
// ) {
// return true;
// }
//
// const parentMenu = await this.findById(parentId);
// if (!parentMenu) {
// throw this.exceptions.menu.parentNotFound(parentId);
// }
// return true;
// } catch (error) {
// if (error.name === 'NotFoundError') {
// throw error;
// }
// this._handleError(error, 'checkParentMenuExists', { parentId });
// }
// }
/**
* 🔥 Menu模块实战:检查菜单类型有效性 - 统一异常处理版本
* @param {String} menuType 菜单类型
* @return {Promise<Boolean>} 菜单类型是否有效
* @throws {ValidationError} 当菜单类型无效时抛出异常
*/
// async checkMenuTypeValid(menuType) {
// try {
// const validTypes = [
// SYSTEM_CONSTANTS.PERMISSION.MENU_TYPE.DIRECTORY,
// SYSTEM_CONSTANTS.PERMISSION.MENU_TYPE.MENU,
// ];
//
// if (!validTypes.includes(menuType)) {
// throw this.exceptions.menu.invalidMenuType(menuType);
// }
// return true;
// } catch (error) {
// if (error.name === 'ValidationError') {
// throw error;
// }
// this._handleError(error, 'checkMenuTypeValid', { menuType });
// }
// }
/**
* 🔥 统一异常处理版本:检查路由路径是否唯一 - 必须使用UniqueChecker
* @param {String} routePath 路由路径
* @param {String} excludeId 排除的ID(用于更新时检查)
* @return {Promise<Boolean>} 是否唯一
* @throws {UniqueConstraintError} 当路由路径已存在时抛出异常
*/
// async checkRoutePathUnique(routePath, excludeId = null) {
// try {
// // 🔥 必须使用UniqueChecker统一处理唯一性验证,自动兼容MongoDB/MariaDB
// const isUnique = await UniqueChecker.checkRoutePathUnique(this, routePath, excludeId);
// if (!isUnique) {
// throw this.exceptions.menu.routePathExists(routePath);
// }
// return true;
// } catch (error) {
// if (error.name === 'UniqueConstraintError') {
// throw error;
// }
// this._handleError(error, 'checkRoutePathUnique', { routePath, excludeId });
// }
// }
/**
* 🔥 统一异常处理版本:检查路由名称是否唯一 - 必须使用UniqueChecker
* @param {String} routeName 路由名称
* @param {String} excludeId 排除的ID(用于更新时检查)
* @return {Promise<Boolean>} 是否唯一
* @throws {UniqueConstraintError} 当路由名称已存在时抛出异常
*/
// async checkRouteNameUnique(routeName, excludeId = null) {
// try {
// // 🔥 必须使用UniqueChecker统一处理唯一性验证,自动兼容MongoDB/MariaDB
// const isUnique = await UniqueChecker.checkRouteNameUnique(this, routeName, excludeId);
// if (!isUnique) {
// throw this.exceptions.menu.routeNameExists(routeName);
// }
// return true;
// } catch (error) {
// if (error.name === 'UniqueConstraintError') {
// throw error;
// }
// this._handleError(error, 'checkRouteNameUnique', { routeName, excludeId });
// }
// }
/**
* 🔥 通用实战:检查按钮代码是否唯一 - 统一异常处理版本
* @param {Array} buttonCodes 按钮代码数组
* @param {String} excludeMenuId 排除的菜单ID
* @return {Promise<Boolean>} 是否唯一
* @throws {UniqueConstraintError} 当按钮代码已存在时抛出异常
*/
// async checkButtonCodesUnique(buttonCodes, excludeMenuId = null) {
// await this._ensureConnection();
//
// try {
// if (!buttonCodes || !Array.isArray(buttonCodes) || buttonCodes.length === 0) {
// return true;
// }
//
// let whereCondition = { status: SYSTEM_CONSTANTS.STATUS.ENABLED };
// if (excludeMenuId) {
// whereCondition.id = { [this.Op.ne]: excludeMenuId };
// }
//
// const menus = await this.model.findAll({
// where: whereCondition,
// attributes: ['buttons'],
// });
//
// // 收集所有现有的按钮代码
// const existingCodes = new Set();
// menus.forEach(menu => {
// const menuJson = this._deepToJSON(menu);
// if (menuJson.buttons && Array.isArray(menuJson.buttons)) {
// menuJson.buttons.forEach(button => {
// if (button.code) {
// existingCodes.add(button.code);
// }
// });
// }
// });
//
// // 检查是否有重复
// const conflictCodes = buttonCodes.filter(code => existingCodes.has(code));
// if (conflictCodes.length > 0) {
// throw this.exceptions.menu.buttonCodeExists(conflictCodes.join(', '));
// }
//
// return true;
// } catch (error) {
// if (error.name === 'UniqueConstraintError') {
// throw error;
// }
// this._handleError(error, 'checkButtonCodesUnique', { buttonCodes, excludeMenuId });
// }
// }
/**
* 🔥 统一异常处理版本:检查字段唯一性 - 必须使用UniqueChecker
* @param {String} userName 用户名
* @param {String} excludeId 排除的ID(用于更新时检查)
* @return {Promise<Boolean>} 是否唯一
* @throws {UniqueConstraintError} 当用户名已存在时抛出异常
*/
// async checkUserNameUnique(userName, excludeId = null) {
// try {
// // 🔥 必须使用UniqueChecker统一处理唯一性验证,自动兼容MongoDB/MariaDB
// const isUnique = await UniqueChecker.checkUserNameUnique(this, userName, excludeId);
// if (!isUnique) {
// throw this.exceptions.user.nameExists(userName);
// }
// return true;
// } catch (error) {
// if (error.name === 'UniqueConstraintError') {
// throw error;
// }
// this._handleError(error, 'checkUserNameUnique', { userName, excludeId });
// }
// }
/**
* 🔥 Admin模块实战:登录验证 - 统一异常处理版本
* @param {String} identifier 登录标识
* @param {String} password 密码
* @param {String} loginType 登录类型
* @return {Promise<Object>} 用户信息
* @throws {AuthenticationError} 认证失败时抛出异常
*/
// async verifyLogin(identifier, password, loginType) {
// try {
// let admin = null;
// switch (loginType) {
// case 'username':
// admin = await this.findByUserName(identifier, {
// fields: ['id', 'userName', 'password', 'status', 'nickName', 'userRoles'],
// includePassword: true,
// });
// break;
// // ... 其他登录方式
// default:
// throw new Error('Invalid login type');
// }
//
// if (!admin) {
// throw this.exceptions.user.notFound();
// }
//
// if (admin.status !== SYSTEM_CONSTANTS.STATUS.ENABLED) {
// throw this.exceptions.user.disabled();
// }
//
// // 验证密码
// const isValidPassword = CryptoUtil.verifyPassword(
// password,
// admin.password,
// this.app.config.encrypt_key
// );
// if (!isValidPassword) {
// throw this.exceptions.user.invalidCredentials();
// }
//
// // 返回管理员信息(不包含密码)
// delete admin.password;
// return admin;
// } catch (error) {
// this._handleError(error, 'verifyLogin', { identifier, loginType });
// }
// }
/**
* 查找单条记录
*/
async findOne(query = {}, options = {}) {
await this._ensureConnection();
try {
// 标准化参数并转换为 MariaDB 格式
const standardParams = this._standardizeParams(
{}, // 空的 payload
{
filters: query,
populate: options.populate || [],
fields: options.fields,
pagination: { isPaging: false },
}
);
// 构建查询选项
const queryOptions = await this._buildQueryOptions(standardParams);
// 执行查询
const result = await this.model.findOne(queryOptions);
// 处理结果
return result ? result.toJSON() : null;
} catch (error) {
this._handleError(error, 'findOne', { query, options });
}
}
/**
* 根据ID查找记录
*/
async findById(id, options = {}) {
const query = { id: id };
return await this.findOne(query, options);
}
/**
* 统计记录数量
*/
async count(filters = {}) {
await this._ensureConnection();
try {
// 标准化参数并转换为 MariaDB 格式
const standardParams = this._standardizeParams(
{}, // 空的 payload
{
filters,
pagination: { isPaging: false },
}
);
// 执行统计
const count = await this.model.count({ where: standardParams.where });
this._logOperation('count', { filters }, count);
return count;
} catch (error) {
this._handleError(error, 'count', { filters });
}
}
/**
* 创建记录
*/
async create(data) {
await this._ensureConnection();
try {
// 预处理数据
const processedData = this._preprocessDataForCreate(data);
// 创建主记录
const result = await this.model.create(processedData);
// 获取完整数据
const fullResult = await this.findById(result.id, {
populate: this._getDefaultPopulate(),
});
this._logOperation('create', { data }, fullResult);
return fullResult;
} catch (error) {
this._handleError(error, 'create', { data });
}
}
/**
* 更新记录(使用智能更新策略)
*/
async update(id, data, options = {}) {
await this._ensureConnection();
try {
// 转换ID
const mariadbId = this.transformer.transformQueryForMariaDB({ id: id }).id;
// 🔥 使用智能更新策略(基于Menu模块经验)
const { processedData, updateOptions } = await this.smartUpdate(mariadbId, data, options);
// 更新主记录
await this.model.update(processedData, updateOptions);
// 获取更新后的完整数据
const fullResult = await this.findById(id, {
populate: this._getDefaultPopulate(),
});
this._logOperation('update', { id, data }, fullResult);
return fullResult;
} catch (error) {
this._handleError(error, 'update', { id, data });
}
}
/**
* 删除记录
*/
async remove(ids, key = 'id') {
await this._ensureConnection();
try {
// 转换ID格式
const mariadbIds = Array.isArray(ids)
? ids.map(id => this.transformer.transformQueryForMariaDB({ id: id }).id)
: this.transformer.transformQueryForMariaDB({ id: ids }).id;
const whereCondition = Array.isArray(mariadbIds)
? { id: { [this.Op.in]: mariadbIds } }
: { id: mariadbIds };
const result = await this.model.destroy({ where: whereCondition });
this._logOperation('remove', { ids, key }, result);
return { deletedCount: result };
} catch (error) {
this._handleError(error, 'remove', { ids, key });
}
}
/**
* 软删除记录
*/
async safeDelete(ids, updateObj = { status: '0' }) {
await this._ensureConnection();
try {
// 转换ID格式
const mariadbIds = Array.isArray(ids)
? ids.map(id => this.transformer.transformQueryForMariaDB({ id: id }).id)
: this.transformer.transformQueryForMariaDB({ id: ids }).id;
const whereCondition = Array.isArray(mariadbIds)
? { id: { [this.Op.in]: mariadbIds } }
: { id: mariadbIds };
const [result] = await this.model.update(updateObj, { where: whereCondition });
this._logOperation('safeDelete', { ids, updateObj }, result);
return { modifiedCount: result };
} catch (error) {
this._handleError(error, 'safeDelete', { ids, updateObj });
}
}
// ===== 业务特有方法示例(参考Menu模块) =====
// 🔥 根据具体业务需要添加特有方法,例如:
// async checkUniqueField(value, excludeId = null) { ... }
// async findByParentId(parentId, options = {}) { ... }
// async buildTree(dataList, parentId = 0) { ... }
// async batchUpdateStatus(ids, status) { ... }
// ===== 辅助方法 =====
/**
* 构建搜索条件(注意参数顺序)
* @private
*/
_buildSearchCondition(searchKeys, keyword) {
if (!keyword || !searchKeys.length) {
return {};
}
const conditions = searchKeys.map(key => ({
[key]: { [this.Op.like]: `%${keyword}%` },
}));
return {
[this.Op.or]: conditions,
};
}
/**
* 获取默认的关联查询配置
* @private
*/
_getDefaultPopulate() {
return [
// 根据需要添加默认的关联查询
// { path: 'author', select: ['userName', 'nickName', 'logo'] },
];
}
/**
* 预处理创建数据
* @param {Object} data 原始数据
* @return {Object} 处理后的数据
* @private
*/
_preprocessDataForCreate(data) {
const processedData = { ...data };
// 设置创建时间
processedData.createdAt = new Date();
processedData.updatedAt = new Date();
// 🔥 设置默认值(基于Menu模块经验)
if (!processedData.status) {
processedData.status = '1';
}
if (typeof processedData.order === 'undefined') {
processedData.order = 0;
}
return processedData;
}
// 🔥 根据具体业务需求添加其他方法...
// 参考Menu模块实现:findByParentId, checkUniqueField, buildTree, batchUpdateStatus 等
}
module.exports = {ModuleName}MariaRepository;
3. MariaDB Schema 模板 ⭐
文件路径: server/app/repository/schemas/mariadb/{ModuleName}Schema.js
基于Menu模块的成功实践,参考MongoDB模型 server/app/model/{ModuleName}.js:
/*
* @Author: AI Assistant
* @Date: 2024-01-XX
* @Last Modified by: AI Assistant
* @Last Modified time: 2024-01-XX
* @Description: {ModuleName} MariaDB Schema 定义 - 基于Menu模块成功经验
*/
'use strict';
const { DataTypes } = require('sequelize');
/**
* {ModuleName} Schema for MariaDB
* {ModuleName}表结构定义 - 基于 MongoDB 模型设计
*/
const {ModuleName}Schema = (sequelize, app) => {
const {ModuleName} = sequelize.define(
'{moduleName}',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '主键ID',
},
// 🔥 核心字段(与 MongoDB 模型一致,参考Menu模块)
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '名称',
validate: {
notEmpty: true,
len: [1, 100],
},
},
// title: {
// type: DataTypes.STRING(200),
// allowNull: true,
// comment: '标题',
// validate: {
// len: [0, 200],
// },
// },
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '描述/备注',
},
// 🔥 常见业务字段(基于Menu模块经验)
parentId: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '父级ID,0为根级',
},
// 统一时间字段(采用 createdAt/updatedAt 命名)
createdAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '创建时间',
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '更新时间',
defaultValue: DataTypes.NOW,
},
// 🔥 状态字段(与 MongoDB 保持一致,基于Menu模块经验)
status: {
type: DataTypes.STRING(10),
defaultValue: '1',
comment: '状态: 0-禁用, 1-启用',
validate: {
isIn: [['0', '1']],
},
},
// 排序字段
order: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序值',
},
},
{
timestamps: false, // 使用自定义时间字段
tableName: '{moduleName}s',
comment: '{ModuleName}表',
// 🔥 添加虚拟字段以保持与 MongoDB 风格的兼容性(基于Menu模块)
getterMethods: {
// 虚拟 URL 字段
url() {
return `/{moduleName}/${this.name || this.id}`;
},
},
// 🔥 对数据验证进行了加强(基于Menu模块经验)
validate: {
nameRequired() {
if (!this.name || this.name.trim() === '') {
throw new Error('名称不能为空');
}
},
statusValid() {
if (!['0', '1'].includes(this.status)) {
throw new Error('状态必须是 0(禁用)或 1(启用)');
}
},
orderValid() {
if (this.order < 0) {
throw new Error('排序值不能为负数');
}
},
},
hooks: {
beforeUpdate(instance) {
instance.updatedAt = new Date();
},
beforeBulkUpdate(options) {
options.attributes.updatedAt = new Date();
},
beforeCreate(instance) {
if (!instance.createdAt) {
instance.createdAt = new Date();
}
if (!inst
*Truncated - read the full file at https://github