编写 Torrent Service 操作方法文件 - Egg.js 实战
在基于 Egg.js 构建的后端应用中, Service 层 主要负责处理业务逻辑,是 Controller 与数据层之间的重要桥梁。 本文将以一个 Torrent(种子)管理模块 为例,详细讲解如何编写一个结构清晰、功能完整的 torrent service 操作文件。

该 Service 主要围绕以下功能展开:
- 种子列表查询(分页 / 条件 / 排序)
- 根据 hash 获取单个种子信息
- 新增、更新、删除种子
- 判断种子是否已存在
- 随机获取种子数据(常用于推荐或发现页)
一、Service 文件结构说明
在 Egg.js 中,Service 通常位于 app/service 目录下,每一个文件对应一个业务模块:
// app/service/torrent.jsService 通过继承 egg.Service ,可以方便地访问:
this.app(应用实例)this.ctx(请求上下文)- 插件能力(如
egg-mysql)
二、Torrent Service 完整实现
下面是完整的 torrent Service 实现代码, 保持原样,不做结构性改动 :
// app/service/torrent.js
'use strict';
const Service = require('egg').Service;
module.exports = class extends Service {
// 获取列表
async get(params) {
const defaultParams = {
page: 1,
rows: 20,
count: false,
orderby: 'id',
order: 'desc',
hash: '',
id: '',
ids: [],
single: false,
};
params = Object.assign(defaultParams, params);
params.page = params.page || 1;
params.rows = params.rows || 10;
let sql = 'select t1.* from torrent t1';
if (params.count) sql = 'select count(*) as count from torrent t1';
sql += ' where 1';
if (params.id) sql += ` and t1.id = ${params.id}`;
if (params.ids.length) sql += ` and t1.id in (${params.ids.join()})`;
if (params.hash) sql += ` and t1.hash = '${params.hash}'`;
if (!params.count) {
sql += ` order by t1.${params.orderby} ${params.order}`;
sql += ` limit ${(params.page - 1) * params.rows}, ${params.rows}`;
}
const val = await this.app.mysql.query(sql);
if (!params.count) {
for (let i = 0; i < val.length; i++) {
val[i].files = JSON.parse(val[i].files);
if (!params.single) {
val[i].files.splice(3, 100);
}
}
}
return params.count ? val[0].count : val;
}
// 根据 hash 获取种子信息
async getByHash(hash) {
const val = await this.get({ hash, single: true });
return val.length ? val[0] : {};
}
// 插入种子信息,用于手动上传种子
async insert(params) {
params.add_date = new Date();
const val = await this.app.mysql.insert('torrent', params);
return val.insertId;
}
// 更新种子信息
async update(params) {
const val = await this.app.mysql.update('torrent', params);
return val.affectedRows;
}
// 删除种子
async delete(id) {
const val = await this.app.mysql.query('delete from torrent where id = ' + id);
return val.affectedRows;
}
// 检查是否由对应 hash 的种子
async hashExists(hash) {
const sql = `select * from torrent where hash = '${hash}'`;
const val = await this.app.mysql.query(sql);
return val.length ? val[0] : null;
}
// 随机种子信息
async random(number = 10) {
const sql = `SELECT * FROM torrent WHERE id >= (
(SELECT MAX(id) FROM torrent) -
(SELECT MIN(id) FROM torrent)
) * RAND() + (SELECT MIN(id) FROM torrent) LIMIT ${number}`;
return this.app.mysql.query(sql);
}
};三、核心方法设计思路解析
1️⃣ get :通用列表查询方法
这是整个 Service 中 最核心、复用性最高 的方法,支持:
- 分页(
page/rows) - 排序(
orderby/order) - 条件过滤(
id/ids/hash) - 统计数量(
count) - 单条 / 列表返回(
single)
关键设计点:
- 默认参数合并 使用
Object.assign,避免调用方漏传参数导致逻辑异常。 - count 查询复用同一套条件 只切换
select字段,不重复写 SQL。 - 文件列表裁剪 非详情页场景下,只保留前几个文件,减少接口数据体积,提高性能。
这种写法在后台管理系统和内容平台中非常常见。
2️⃣ getByHash :业务语义封装
async getByHash(hash) {
const val = await this.get({ hash, single: true });
return val.length ? val[0] : {};
}这里并没有重新写 SQL,而是 复用 get 方法 ,体现了:
- 逻辑集中
- 代码可维护性高
- 业务语义清晰(通过方法名表达用途)
3️⃣ insert / update :数据写入操作
insert:用于手动上传种子update:用于修改种子信息(如分类、描述、状态等)
在 insert 中自动补充 add_date ,可以避免 Controller 层关心数据库字段细节。
4️⃣ delete :直接删除记录
async delete(id) {
const val = await this.app.mysql.query('delete from torrent where id = ' + id);
return val.affectedRows;
}适用于后台管理场景。 如果是对数据安全要求更高的系统,也可以改成 软删除 (增加 deleted 字段)。
5️⃣ hashExists :防重复校验
Torrent 的 hash 天然唯一 ,这是一个非常典型的业务校验方法,常用于:
- 上传前校验
- 自动抓取时去重
- 同步任务防止重复写入
6️⃣ random :随机推荐数据
该方法通过 ID 区间 + RAND() 的方式实现随机查询,性能优于直接 ORDER BY RAND() ,适合数据量较大的表。
常见使用场景包括:
- 首页推荐
- 你可能感兴趣
- 发现页内容填充
四、实践建议与注意事项
- ⚠ SQL 注入风险 当前代码使用字符串拼接,生产环境建议使用:
this.app.mysql.escape- 或参数化查询
- 📦 字段结构设计
files使用 JSON 字符串存储,适合文件列表这类结构化但不常更新的数据。 - 🔁 Service 层只做业务,不做参数校验 参数校验更适合放在 Controller 或 Middleware 中。
五、总结
本文通过一个完整的 torrent service 示例,展示了在 Egg.js 中如何:
- 设计通用查询方法
- 封装清晰的业务语义
- 合理组织 CRUD 操作
- 平衡灵活性与可维护性
这种 Service 写法不仅适用于 Torrent 系统,也非常适合 内容管理、资源库、媒体系统 等场景,具备很强的可复用价值。
如果你后续想把这套逻辑拆成 微服务 、接入 消息队列 或做 自动化抓取 ,这个 Service 结构也能很好地支撑演进。




