使用案例:从 Controller 到 Mapper
本文按 HTTP 入口 → 业务层 → 数据访问 的顺序,演示如何新增一个完整业务模块。运行时扫描顺序是 entity → mapper → service → controller,但日常开发通常从接口往下写,因此文档也按这个心智组织。
示例业务:文章 Article(虚构模块,写法与仓库内 user / test 一致)。
目标接口:
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | /api/article/list | 登录 | 列表(可选标题筛选) |
| GET | /api/article/:id | 忽略 | 详情 |
| POST | /api/article | 权限 article:add | 新增 |
| DELETE | /api/article/:id | 角色 admin | 删除(逻辑删除) |
文件最终落在:
src/business/
├── entity/article.ts
├── mapper/article.ts
├── service/article.ts
└── controller/article/index.ts权限与登录有两种写法:
- 装饰器:
@AuthCheckLogin/@AuthCheckRole/@AuthCheckPermission/@AuthIgnore - 函数式:在方法内调用
AuthUtil.checkLogin()/checkRole()/checkPermission()等
两者可混用;装饰器适合声明整接口策略,函数式适合分支判断或登录/登出这类流程。
1. Controller:定义接口(装饰器)
路径:src/business/controller/article/index.ts
import {
AuthCheckLogin,
AuthCheckPermission,
AuthCheckRole,
AuthIgnore,
Controller,
DeleteController,
GetController,
PostController,
RequestBody,
RequestPath,
RequestQuery,
} from '@/core/Application'
import { QueryWrapper } from '@/core/ORM'
import { Resource } from '@/core/Service'
import ArticleService from '@/business/service/article'
@Controller('/api/article')
export default class ArticleController {
@Resource('ArticleService')
articleService!: ArticleService
/** 列表:只需登录 */
@GetController('/list')
@AuthCheckLogin()
async list(@RequestQuery() query: { title?: string }) {
const wrapper = new QueryWrapper().select('id', 'title', 'create_time')
if (query?.title) {
wrapper.like('title', query.title)
}
wrapper.orderByDesc('create_time')
return this.articleService.selectList(wrapper)
}
/** 详情:公开 */
@GetController('/:id')
@AuthIgnore()
async detail(@RequestPath() id: string | number) {
return this.articleService.selectById(id)
}
/** 新增:需具备权限 article:add(全部权限都要有) */
@PostController('/')
@AuthCheckPermission(['article:add'])
async create(@RequestBody() body: { title: string; content?: string }) {
return this.articleService.insert(body)
}
/** 删除:需具备角色 admin(角色命中其一即可) */
@DeleteController('/:id')
@AuthCheckRole(['admin'])
async remove(@RequestPath() id: string | number) {
return this.articleService.deleteById(id)
}
}要点:
@AuthCheckLogin():只校验登录@AuthCheckRole(['admin', 'editor']):角色 任一 命中即通过@AuthCheckPermission(['article:add', 'article:edit']):列出的权限需 全部 具备@AuthIgnore():跳过登录- 静态路由
/list写在/:id前面
鉴权装饰器与 AuthUtil 函数式用法见 Application 与装饰器、鉴权 Auth;完整走读含权限与函数式示例见 使用示例。
2. 权限与登录:函数式 AuthUtil
不依赖方法装饰器时,在业务代码里直接调 AuthUtil。登录时写入的 roles / permissions,会进入当前会话,供后续校验使用。
2.1 登录 / 登出
import { AuthIgnore, Controller, PostController, RequestBody } from '@/core/Application'
import { AuthUtil } from '@/core/Auth'
@Controller('/auth')
export default class AuthController {
/** 登录:校验账号后发放 token,并写入角色与权限 */
@PostController('/login')
@AuthIgnore()
async login(
@RequestBody() body: { username: string; password: string }
) {
// 此处省略查库验密,假设 userId = 1
const userId = 1
const roles = ['admin']
const permissions = ['article:add', 'article:edit', 'article:delete']
// 签名:login(loginId, roles?, permissions?)
const token = await AuthUtil.login(userId, roles, permissions)
return { token }
}
/** 退出当前会话 */
@PostController('/logout')
async logout() {
await AuthUtil.checkLogin()
await AuthUtil.logout()
return true
}
}请求头携带:Authorization: Bearer <token>。
入口已预留全局 ignore:Auth({ ignore: ['/auth/login'] }),与上方 @AuthIgnore() 二选一即可。
2.2 在接口内函数式校验
适合「同一接口按条件分支鉴权」,或不用装饰器时:
import { Controller, DeleteController, GetController, PostController, RequestBody, RequestPath } from '@/core/Application'
import { AuthUtil } from '@/core/Auth'
import { Resource } from '@/core/Service'
import ArticleService from '@/business/service/article'
@Controller('/api/article-fn')
export default class ArticleFnController {
@Resource('ArticleService')
articleService!: ArticleService
/** 等价于 @AuthCheckLogin() */
@GetController('/list')
async list() {
await AuthUtil.checkLogin()
return this.articleService.selectList()
}
/** 等价于 @AuthCheckRole(['admin']) */
@DeleteController('/:id')
async remove(@RequestPath() id: string | number) {
await AuthUtil.checkRole('admin')
return this.articleService.deleteById(id)
}
/** 等价于 @AuthCheckPermission(['article:add']) */
@PostController('/')
async create(@RequestBody() body: { title: string }) {
await AuthUtil.checkPermission('article:add')
return this.articleService.insert(body)
}
/** 分支判断:用 has* 而不是抛错的 check* */
@GetController('/mine')
async mine() {
if (!(await AuthUtil.isLogin())) {
return { guest: true }
}
const loginId = await AuthUtil.getLoginId()
const roles = await AuthUtil.getRoleList()
const permissions = await AuthUtil.getPermissionList()
const isAdmin = await AuthUtil.hasRole('admin')
const canEdit = await AuthUtil.hasPermission('article:edit')
return { loginId, roles, permissions, isAdmin, canEdit }
}
}2.3 AuthUtil API 速查
| 方法 | 作用 |
|---|---|
login(loginId, options) / 旧签名 login(id, roles?, permissions?) | 登录,返回 token;可传 device / timeout 等 |
logout(token?) / logoutByLoginId / logoutByDevice | 注销当前、按账号、按设备 |
getLoginDevice() | 当前登录设备 |
logoutByLoginId(loginId) | 按账号踢全部会话 |
isLogin() | 是否已登录 |
checkLogin() | 未登录抛错,返回 loginId |
getLoginId() / getLoginIdDefaultNull() | 取当前账号 |
getRoleList() / getPermissionList() | 当前会话角色 / 权限 |
hasRole / hasPermission | 布尔判断 |
checkRole / checkPermission | 失败抛 NotRoleError / NotPermissionError |
getTokenValue() / getSession() | 读 token / 会话 |
更完整说明见 鉴权 Auth。
3. 函数式注册路由(不用 @Controller)
Application 也提供 GET / POST / PUT / DELETE,可在 index.ts 或任意模块里链式注册。权限写在第三个参数 option.auth:
import Application, { ParamType } from '@/core/Application'
import { AuthUtil } from '@/core/Auth'
import { Container } from '@/core/Service'
import ArticleService from '@/business/service/article'
// 需先加载 business,保证 ArticleService 已进容器
const articleService = Container.get<ArticleService>('ArticleService')
// 忽略登录
Application.GET(
'/api/article-raw/:id',
async (req) => articleService.selectById(req.params.id),
{ auth: { ignore: true } }
)
// 仅登录;query 注入到回调第一个参数
Application.GET(
'/api/article-raw/list',
async (query: { title?: string }) => {
// 也可在回调里再函数式校验
await AuthUtil.checkLogin()
return articleService.selectList()
},
{ auth: {}, paramType: ParamType.QUERY }
)
// 角色:任一命中
Application.DELETE(
'/api/article-raw/:id',
async (req) => articleService.deleteById(req.params.id),
{ auth: { roles: ['admin'] } }
)
// 权限:需全部具备
Application.POST(
'/api/article-raw',
async (body: { title: string }) => articleService.insert(body),
{ auth: { permissions: ['article:add'] }, paramType: ParamType.BODY }
)与装饰器对照:
| 能力 | 装饰器 | 函数式 |
|---|---|---|
| 登录 | @AuthCheckLogin() | auth: {} + 框架 checkLogin,或手写 AuthUtil.checkLogin() |
| 角色 | @AuthCheckRole(['admin']) | auth: { roles: ['admin'] } 或 AuthUtil.checkRole('admin') |
| 权限 | @AuthCheckPermission(['a']) | auth: { permissions: ['a'] } 或 AuthUtil.checkPermission('a') |
| 忽略 | @AuthIgnore() | auth: { ignore: true } |
| 取 Bean | @Resource('ArticleService') | Container.get('ArticleService') |
4. Service:承接业务
路径:src/business/service/article.ts
import { BaseService, Service } from '@/core/Service'
import ArticleMapper from '@/business/mapper/article'
@Service(ArticleMapper)
export default class ArticleService extends BaseService {
// 自定义 Mapper 方法可在此 declare,例如:
// declare findByTitle: (title: string) => Promise<Record<string, unknown>[]>
}Controller / 函数式路由里调用的 selectList / selectById / insert / deleteById 均由 @Service 从 Mapper 挂载。
5. Mapper:访问数据库
路径:src/business/mapper/article.ts
import { Mapper, MapperType, Param, Select } from '@/core/Service'
import ArticleEntity from '@/business/entity/article'
@Mapper(ArticleEntity, { logicDelete: true, fill: true })
export default class ArticleMapper extends MapperType {
@Select(
'SELECT id, title FROM article WHERE title = #{title} AND delete_flag = 0'
)
findByTitle(
@Param('title') _title: string
): Promise<Record<string, unknown>[]> {
return undefined as never
}
}6. Entity:表映射
路径:src/business/entity/article.ts
import { TableField, TableId, TableName } from '@/core/ORM'
@TableName('article')
export default class ArticleEntity {
@TableId()
id!: number | string
@TableField()
title!: string
@TableField()
content?: string
@TableField({
value: 'create_time',
insertFill: () => new Date(),
})
createTime?: Date
@TableField({
value: 'update_time',
insertFill: () => new Date(),
updateFill: () => new Date(),
})
updateTime?: Date
@TableField({
value: 'delete_flag',
insertFill: 0,
})
deleteFlag?: number
}CREATE TABLE article (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
create_time DATETIME NULL,
update_time DATETIME NULL,
delete_flag TINYINT NOT NULL DEFAULT 0
);7. 调用链小结
登录:POST /auth/login
→ AuthUtil.login(userId, roles, permissions) → 返回 token
业务:DELETE /api/article/1 Authorization: Bearer <token>
→ @AuthCheckRole(['admin']) 或 AuthUtil.checkRole('admin')
→ ArticleService.deleteById
→ ArticleMapper(逻辑删除)
→ ResponseData检查清单:
- Controller 声明路径,并用装饰器或
AuthUtil做登录/角色/权限 - 登录时把角色、权限写入
AuthUtil.login - Service
@Service(XxxMapper),与@Resource/Container.get名称一致 - Mapper / Entity 对齐表结构
- 重启
npm run dev
对照仓库现有示例
| 层级 | User 模块 | Test 模块 |
|---|---|---|
| Controller | business/controller/user | business/controller/test |
| Service | business/service/user.ts | business/service/test.ts |
| Mapper | business/mapper/user.ts | business/mapper/test.ts(含 @Select) |
| Entity | business/entity/user.ts | business/entity/test.ts |