diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..fec5140
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+# 使用官方 Bun 运行时的轻量级镜像
+FROM oven/bun:alpine as base
+
+WORKDIR /app
+
+# 仅复制生产依赖相关文件
+COPY package.json bun.lockb knexfile.mjs .npmrc ./
+
+# 安装依赖(生产环境)
+RUN bun install --production
+
+# 复制应用代码和静态资源
+COPY src ./src
+COPY public ./public
+
+COPY entrypoint.sh ./entrypoint.sh
+RUN chmod +x ./entrypoint.sh
+
+# 如需数据库文件(如 SQLite),可挂载到宿主机
+VOLUME /app/database
+
+# 启动命令(如有端口需求可暴露端口)
+EXPOSE 3000
+
+# 健康检查:每30秒检查一次服务端口,3次失败则容器为unhealthy
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
+ CMD wget --spider -q http://localhost:3000/ || exit 1
+
+ENTRYPOINT ["./entrypoint.sh"]
diff --git a/bun.lockb b/bun.lockb
index 2739946..7be0543 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/database/.gitkeep b/database/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/database/development.sqlite3 b/database/development.sqlite3
index 5238449..3f03792 100644
Binary files a/database/development.sqlite3 and b/database/development.sqlite3 differ
diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm
index 2553477..30b5938 100644
Binary files a/database/development.sqlite3-shm and b/database/development.sqlite3-shm differ
diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal
index bea45f3..8e6f511 100644
Binary files a/database/development.sqlite3-wal and b/database/development.sqlite3-wal differ
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100644
index 0000000..57d99b5
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -e
+
+# 数据库文件路径(可根据实际环境调整)
+DB_FILE=./database/db.sqlite3
+
+# 如果数据库文件不存在,先 migrate 再 seed
+if [ ! -f "$DB_FILE" ]; then
+ echo "Database not found, running migration and seed..."
+ bun run npx knex migrate:latest
+ bun run npx knex seed:run
+else
+ echo "Database exists, running migration only..."
+ bun run npx knex migrate:latest
+fi
+
+# 启动主服务
+exec bun src/main.js
diff --git a/package.json b/package.json
index 20f4fe0..12ba189 100644
--- a/package.json
+++ b/package.json
@@ -12,12 +12,16 @@
},
"devDependencies": {
"@types/bun": "latest",
- "@types/node": "^24.0.1",
- "knex": "^3.1.0"
+ "@types/node": "^24.0.1"
},
"dependencies": {
+ "bcryptjs": "^3.0.2",
+ "jsonwebtoken": "^9.0.0",
+ "knex": "^3.1.0",
"koa": "^3.0.0",
+ "koa-bodyparser": "^4.4.1",
"log4js": "^6.9.1",
+ "minimatch": "^9.0.0",
"module-alias": "^2.2.3",
"node-cron": "^4.1.0",
"path-to-regexp": "^8.2.0",
diff --git a/public/aa.txt b/public/aa.txt
deleted file mode 100644
index 6a12326..0000000
--- a/public/aa.txt
+++ /dev/null
@@ -1 +0,0 @@
-sada
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..535e0ac
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+ 登录 / 注册
+
+
+
+
+
+
+
diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js
index be3cb2b..3348cf8 100644
--- a/src/controllers/JobController.js
+++ b/src/controllers/JobController.js
@@ -1,41 +1,56 @@
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
import JobService from "services/JobService.js"
-import Router from "utils/router.js"
+import { formatResponse } from "utils/helper.js"
-class JobController {
- static routes() {
- const router = new Router({ prefix: "/api/jobs" })
- router.get("/", JobController.list)
- router.post("/start/:id", JobController.start)
- router.post("/stop/:id", JobController.stop)
- router.post("/update/:id", JobController.updateCron)
- return router
- }
+const jobService = new JobService()
- static async list(ctx) {
- ctx.body = JobService.listJobs()
+export const list = async (ctx) => {
+ try {
+ const data = jobService.listJobs()
+ ctx.body = formatResponse(true, data)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message || "获取任务列表失败")
}
+}
- static async start(ctx) {
- const { id } = ctx.params
- JobService.startJob(id)
- ctx.body = { success: true, message: `${id} 任务已启动` }
+export const start = async (ctx) => {
+ const { id } = ctx.params
+ try {
+ jobService.startJob(id)
+ ctx.body = formatResponse(true, null, null, `${id} 任务已启动`)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message || "启动任务失败")
}
+}
- static async stop(ctx) {
- const { id } = ctx.params
- JobService.stopJob(id)
- ctx.body = { success: true, message: `${id} 任务已停止` }
+export const stop = async (ctx) => {
+ const { id } = ctx.params
+ try {
+ jobService.stopJob(id)
+ ctx.body = formatResponse(true, null, null, `${id} 任务已停止`)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message || "停止任务失败")
}
+}
- static async updateCron(ctx) {
- const { id } = ctx.params
- const { cronTime } = ctx.request.body
- JobService.updateJobCron(id, cronTime)
- ctx.body = { success: true, message: `${id} 任务频率已修改` }
+export const updateCron = async (ctx) => {
+ const { id } = ctx.params
+ const { cronTime } = ctx.request.body
+ try {
+ jobService.updateJobCron(id, cronTime)
+ ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message || "修改任务频率失败")
}
}
-export default JobController
-
-// 你可以在路由中引入这些 controller 方法,实现接口调用
+// 路由注册示例
+import Router from "utils/router.js"
+export function createRoutes() {
+ const router = new Router({ prefix: "/api/jobs" })
+ router.get("/", list)
+ router.post("/start/:id", start)
+ router.post("/stop/:id", stop)
+ router.post("/update/:id", updateCron)
+ return router
+}
diff --git a/src/controllers/StatusController.js b/src/controllers/StatusController.js
index f6775ed..5239ec0 100644
--- a/src/controllers/StatusController.js
+++ b/src/controllers/StatusController.js
@@ -1,22 +1,16 @@
-import Router from 'utils/router.js';
+import { formatResponse } from "utils/helper.js"
-class StatusController {
- static routes() {
- const v1 = new Router({ prefix: "/api/v1" })
-
- // 组内中间件
- v1.use((ctx, next) => {
- ctx.set("X-API-Version", "v1")
- return next()
- })
-
- v1.get("/status", StatusController.status)
- return v1
- }
-
- static async status(ctx) {
- ctx.body = "OK"
- }
+export const status = async (ctx) => {
+ ctx.body = "OK"
}
-export default StatusController
+import Router from "utils/router.js"
+export function createRoutes() {
+ const v1 = new Router({ prefix: "/api/v1" })
+ v1.use((ctx, next) => {
+ ctx.set("X-API-Version", "v1")
+ return next()
+ })
+ v1.get("/status", status)
+ return v1
+}
diff --git a/src/controllers/userController.js b/src/controllers/userController.js
index 91a9527..b00549d 100644
--- a/src/controllers/userController.js
+++ b/src/controllers/userController.js
@@ -1,23 +1,44 @@
-import UserService from 'services/UserService.js';
-import Router from 'utils/router.js';
+import UserService from "services/UserService.js"
+import { formatResponse } from "utils/helper.js"
-class UserController {
- static routes() {
- let router = new Router({ prefix: '/api' });
- router.get('/hello', UserController.hello);
- router.get('/user/:id', UserController.getUser);
- return router;
- }
+const userService = new UserService()
- static async hello(ctx) {
- ctx.body = 'Hello World';
- }
+export const hello = async (ctx) => {
+ ctx.body = formatResponse(true, "Hello World")
+}
+
+export const getUser = async (ctx) => {
+ const user = await userService.getUserById(ctx.params.id)
+ ctx.body = formatResponse(true, user)
+}
- static async getUser(ctx) {
- // 调用 service 层获取用户
- const user = await UserService.getUserById(ctx.params.id);
- ctx.body = user;
- }
+export const register = async (ctx) => {
+ try {
+ const { username, email, password } = ctx.request.body
+ const user = await userService.register({ username, email, password })
+ ctx.body = formatResponse(true, user)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message)
+ }
}
-export default UserController;
\ No newline at end of file
+export const login = async (ctx) => {
+ try {
+ const { username, email, password } = ctx.request.body
+ const result = await userService.login({ username, email, password })
+ ctx.body = formatResponse(true, result)
+ } catch (err) {
+ ctx.body = formatResponse(false, null, err.message)
+ }
+}
+
+// 路由注册示例
+import Router from "utils/router.js"
+export function createRoutes() {
+ const router = new Router({ prefix: "/api" })
+ router.get("/hello", hello)
+ router.get("/user/:id", getUser)
+ router.post("/register", register)
+ router.post("/login", login)
+ return router
+}
diff --git a/src/db/migrations/20250616065041_create_users_table.mjs b/src/db/migrations/20250616065041_create_users_table.mjs
index 56f7418..f73fcae 100644
--- a/src/db/migrations/20250616065041_create_users_table.mjs
+++ b/src/db/migrations/20250616065041_create_users_table.mjs
@@ -7,6 +7,7 @@ export const up = async knex => {
table.increments("id").primary() // 自增主键
table.string("username", 100).notNullable() // 字符串字段(最大长度100)
table.string("email", 100).unique().notNullable() // 唯一邮箱
+ table.string("password", 100).unique() // 密码
table.integer("age").unsigned() // 无符号整数
table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间
table.timestamp("updated_at").defaultTo(knex.fn.now()) // 更新时间
diff --git a/src/db/models/UserModel.js b/src/db/models/UserModel.js
index f263586..19de0ac 100644
--- a/src/db/models/UserModel.js
+++ b/src/db/models/UserModel.js
@@ -20,6 +20,13 @@ class UserModel {
static async delete(id) {
return db("users").where("id", id).del()
}
+
+ static async findByUsername(username) {
+ return db("users").where("username", username).first()
+ }
+ static async findByEmail(email) {
+ return db("users").where("email", email).first()
+ }
}
export default UserModel
diff --git a/src/main.js b/src/main.js
index b6f4296..65e5d4b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -8,14 +8,16 @@ import os from "os"
import log4js from "log4js"
// 应用插件与自动路由
-import LoadPlugins from "./plugins/install.js"
+import LoadMiddlewares from "./middlewares/install.js"
import { autoRegisterControllers } from "utils/autoRegister.js"
+import bodyParser from "koa-bodyparser"
const logger = log4js.getLogger()
const app = new Koa()
+app.use(bodyParser());
// 注册插件
-LoadPlugins(app)
+LoadMiddlewares(app)
// 自动注册所有 controller
autoRegisterControllers(app)
diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js
new file mode 100644
index 0000000..779d3fd
--- /dev/null
+++ b/src/middlewares/Auth/auth.js
@@ -0,0 +1,68 @@
+// JWT 鉴权中间件,支持白名单和黑名单,白名单/黑名单支持glob语法,白名单可指定是否校验权限(auth: true/false/"try")
+import jwt from "./jwt"
+import { minimatch } from "minimatch"
+
+export const JWT_SECRET = process.env.JWT_SECRET || "jwt-demo-secret"
+
+function matchList(list, path) {
+ for (const item of list) {
+ if (typeof item === "string" && minimatch(path, item)) {
+ return { matched: true, auth: false }
+ }
+ if (typeof item === "object" && minimatch(path, item.pattern)) {
+ return { matched: true, auth: item.auth }
+ }
+ }
+ return { matched: false }
+}
+
+function verifyToken(ctx) {
+ const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "")
+ if (!token) return { ok: false }
+ try {
+ ctx.state.user = jwt.verify(token, JWT_SECRET)
+ return { ok: true }
+ } catch {
+ ctx.state.user = undefined
+ return { ok: false }
+ }
+}
+
+export default function authMiddleware(options = {
+ whiteList: [],
+ blackList: []
+}) {
+ return async (ctx, next) => {
+ // 黑名单优先生效
+ if (matchList(options.blackList, ctx.path).matched) {
+ ctx.status = 403
+ ctx.body = { success: false, error: "禁止访问" }
+ return
+ }
+ // 白名单处理
+ const white = matchList(options.whiteList, ctx.path)
+ if (white.matched) {
+ if (white.auth === false) {
+ return await next()
+ }
+ if (white.auth === "try") {
+ verifyToken(ctx) // token可选,校验失败不报错
+ return await next()
+ }
+ // true 或其他情况,必须有token
+ if (!verifyToken(ctx).ok) {
+ ctx.status = 401
+ ctx.body = { success: false, error: "未登录或token缺失或无效" }
+ return
+ }
+ return await next()
+ }
+ // 非白名单,必须有token
+ if (!verifyToken(ctx).ok) {
+ ctx.status = 401
+ ctx.body = { success: false, error: "未登录或token缺失或无效" }
+ return
+ }
+ await next()
+ }
+}
diff --git a/src/middlewares/Auth/index.js b/src/middlewares/Auth/index.js
new file mode 100644
index 0000000..7e8009b
--- /dev/null
+++ b/src/middlewares/Auth/index.js
@@ -0,0 +1,3 @@
+// 统一导出所有中间件
+import auth from "./auth.js"
+export { auth }
diff --git a/src/middlewares/Auth/jwt.js b/src/middlewares/Auth/jwt.js
new file mode 100644
index 0000000..0af32e5
--- /dev/null
+++ b/src/middlewares/Auth/jwt.js
@@ -0,0 +1,3 @@
+// 兼容性导出,便于后续扩展
+import jwt from "jsonwebtoken"
+export default jwt
diff --git a/src/plugins/ResponseTime/index.js b/src/middlewares/ResponseTime/index.js
similarity index 100%
rename from src/plugins/ResponseTime/index.js
rename to src/middlewares/ResponseTime/index.js
diff --git a/src/plugins/Send/index.js b/src/middlewares/Send/index.js
similarity index 100%
rename from src/plugins/Send/index.js
rename to src/middlewares/Send/index.js
diff --git a/src/plugins/Send/resolve-path.js b/src/middlewares/Send/resolve-path.js
similarity index 100%
rename from src/plugins/Send/resolve-path.js
rename to src/middlewares/Send/resolve-path.js
diff --git a/src/plugins/errorHandler/index.js b/src/middlewares/errorHandler/index.js
similarity index 100%
rename from src/plugins/errorHandler/index.js
rename to src/middlewares/errorHandler/index.js
diff --git a/src/middlewares/install.js b/src/middlewares/install.js
new file mode 100644
index 0000000..0db2696
--- /dev/null
+++ b/src/middlewares/install.js
@@ -0,0 +1,33 @@
+import ResponseTime from "./ResponseTime"
+import Send from "./Send"
+import { resolve } from "path"
+import { fileURLToPath } from "url"
+import path from "path"
+import errorHandler from "./errorHandler"
+import { auth } from "./Auth"
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const publicPath = resolve(__dirname, "../../public")
+
+export default app => {
+ app.use(errorHandler())
+ app.use(ResponseTime)
+ app.use(
+ auth({
+ whiteList: [
+ { pattern: "/", auth: "try" },
+ "/api/login",
+ "/api/register"
+ ],
+ blackList: [],
+ })
+ )
+ app.use(async (ctx, next) => {
+ try {
+ await Send(ctx, ctx.path, { root: publicPath })
+ } catch (err) {
+ if (err.status !== 404) throw err
+ }
+ await next()
+ })
+}
diff --git a/src/plugins/install.js b/src/plugins/install.js
deleted file mode 100644
index 7ef8498..0000000
--- a/src/plugins/install.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import ResponseTime from "./ResponseTime";
-import Send from "./Send";
-import { resolve } from 'path';
-import { fileURLToPath } from 'url';
-import path from "path";
-import errorHandler from './errorHandler';
-
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const publicPath = resolve(__dirname, '../../public');
-
-export default (app)=>{
- app.use(errorHandler());
- app.use(ResponseTime)
- app.use(async (ctx, next) => {
- try {
- await Send(ctx, ctx.path, { root: publicPath });
- } catch (err) {
- if (err.status !== 404) throw err;
- }
- await next();
- })
-}
\ No newline at end of file
diff --git a/src/services/JobService.js b/src/services/JobService.js
index 1649aeb..35a04a3 100644
--- a/src/services/JobService.js
+++ b/src/services/JobService.js
@@ -1,18 +1,18 @@
-import jobs from '../jobs';
+import jobs from "../jobs"
class JobService {
- static startJob(id) {
- return jobs.start(id);
- }
- static stopJob(id) {
- return jobs.stop(id);
- }
- static updateJobCron(id, cronTime) {
- return jobs.updateCronTime(id, cronTime);
- }
- static listJobs() {
- return jobs.list();
- }
+ startJob(id) {
+ return jobs.start(id)
+ }
+ stopJob(id) {
+ return jobs.stop(id)
+ }
+ updateJobCron(id, cronTime) {
+ return jobs.updateCronTime(id, cronTime)
+ }
+ listJobs() {
+ return jobs.list()
+ }
}
-export default JobService;
\ No newline at end of file
+export default JobService
diff --git a/src/services/userService.js b/src/services/userService.js
index 1bd0a77..960ff1d 100644
--- a/src/services/userService.js
+++ b/src/services/userService.js
@@ -1,39 +1,74 @@
-// src/services/userService.js
-// 用户相关业务逻辑
-
-import UserModel from 'db/models/UserModel.js';
+import UserModel from "db/models/UserModel.js"
+import { hashPassword, comparePassword } from "utils/bcrypt.js"
+import { JWT_SECRET } from "@/middlewares/Auth/auth.js"
+import jwt from "@/middlewares/Auth/jwt.js"
class UserService {
- static async getUserById(id) {
- // 这里可以调用数据库模型
- // 示例返回
- return { id, name: `User_${id}` };
- }
-
- // 获取所有用户
- static async getAllUsers() {
- return await UserModel.findAll();
- }
-
- // 创建新用户
- static async createUser(data) {
- if (!data.name) throw new Error('用户名不能为空');
- return await UserModel.create(data);
- }
-
- // 更新用户
- static async updateUser(id, data) {
- const user = await UserModel.findById(id);
- if (!user) throw new Error('用户不存在');
- return await UserModel.update(id, data);
- }
-
- // 删除用户
- static async deleteUser(id) {
- const user = await UserModel.findById(id);
- if (!user) throw new Error('用户不存在');
- return await UserModel.delete(id);
- }
+ async getUserById(id) {
+ // 这里可以调用数据库模型
+ // 示例返回
+ return { id, name: `User_${id}` }
+ }
+
+ // 获取所有用户
+ async getAllUsers() {
+ return await UserModel.findAll()
+ }
+
+ // 创建新用户
+ async createUser(data) {
+ if (!data.name) throw new Error("用户名不能为空")
+ return await UserModel.create(data)
+ }
+
+ // 更新用户
+ async updateUser(id, data) {
+ const user = await UserModel.findById(id)
+ if (!user) throw new Error("用户不存在")
+ return await UserModel.update(id, data)
+ }
+
+ // 删除用户
+ async deleteUser(id) {
+ const user = await UserModel.findById(id)
+ if (!user) throw new Error("用户不存在")
+ return await UserModel.delete(id)
+ }
+
+ // 注册新用户
+ async register(data) {
+ if (!data.username || !data.email || !data.password) throw new Error("用户名、邮箱和密码不能为空")
+ const existUser = await UserModel.findByUsername(data.username)
+ if (existUser) throw new Error("用户名已存在")
+ const existEmail = await UserModel.findByEmail(data.email)
+ if (existEmail) throw new Error("邮箱已被注册")
+ // 密码加密
+ const hashed = await hashPassword(data.password)
+
+ const user = await UserModel.create({ ...data, password: hashed })
+ // 返回脱敏信息
+ const { password, ...userInfo } = Array.isArray(user) ? user[0] : user
+ return userInfo
+ }
+
+ // 登录
+ async login({ username, email, password }) {
+ let user
+ if (username) {
+ user = await UserModel.findByUsername(username)
+ } else if (email) {
+ user = await UserModel.findByEmail(email)
+ }
+ if (!user) throw new Error("用户不存在")
+ // 校验密码
+ const ok = await comparePassword(password, user.password)
+ if (!ok) throw new Error("密码错误")
+ // 生成token
+ const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" })
+ // 返回token和用户信息
+ const { password: pwd, ...userInfo } = user
+ return { token, user: userInfo }
+ }
}
-export default UserService;
+export default UserService
diff --git a/src/utils/BaseSingleton.js b/src/utils/BaseSingleton.js
new file mode 100644
index 0000000..9705647
--- /dev/null
+++ b/src/utils/BaseSingleton.js
@@ -0,0 +1,37 @@
+// 抽象基类,使用泛型来正确推导子类类型
+class BaseSingleton {
+ static _instance
+
+ constructor() {
+ if (this.constructor === BaseSingleton) {
+ throw new Error("禁止直接实例化 BaseOne 抽象类")
+ }
+
+ if (this.constructor._instance) {
+ throw new Error("构造函数私有化失败,禁止重复 new")
+ }
+
+ // this.constructor 是子类,所以这里设为 instance
+ this.constructor._instance = this
+ }
+
+ static getInstance() {
+ const clazz = this
+ if (!clazz._instance) {
+ const self = new this()
+ const handler = {
+ get: function (target, prop) {
+ const value = Reflect.get(target, prop)
+ if (typeof value === "function") {
+ return value.bind(target)
+ }
+ return Reflect.get(target, prop)
+ },
+ }
+ clazz._instance = new Proxy(self, handler)
+ }
+ return clazz._instance
+ }
+}
+
+export { BaseSingleton }
diff --git a/src/utils/autoRegister.js b/src/utils/autoRegister.js
index 4390aba..64b7b30 100644
--- a/src/utils/autoRegister.js
+++ b/src/utils/autoRegister.js
@@ -26,7 +26,7 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir
} catch (e) {
controller = (await import(fullPath)).default
}
- const routes = controller.routes || controller.default?.routes || controller.default || controller
+ const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller
// 判断 routes 方法参数个数,支持自动适配
if (typeof routes === "function") {
allRouter.push(routes())
diff --git a/src/utils/bcrypt.js b/src/utils/bcrypt.js
new file mode 100644
index 0000000..4c26d52
--- /dev/null
+++ b/src/utils/bcrypt.js
@@ -0,0 +1,11 @@
+// 密码加密与校验工具
+import bcrypt from "bcryptjs"
+
+export async function hashPassword(password) {
+ const salt = await bcrypt.genSalt(10)
+ return bcrypt.hash(password, salt)
+}
+
+export async function comparePassword(password, hash) {
+ return bcrypt.compare(password, hash)
+}
diff --git a/src/utils/helper.js b/src/utils/helper.js
new file mode 100644
index 0000000..a1903ee
--- /dev/null
+++ b/src/utils/helper.js
@@ -0,0 +1,4 @@
+
+export function formatResponse(success, data = null, error = null) {
+ return { success, error, data }
+}