Skip to content

使用案例:从 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

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 登录 / 登出

ts
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 在接口内函数式校验

适合「同一接口按条件分支鉴权」,或不用装饰器时:

ts
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

ts
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

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

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

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
}
sql
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

检查清单:

  1. Controller 声明路径,并用装饰器或 AuthUtil 做登录/角色/权限
  2. 登录时把角色、权限写入 AuthUtil.login
  3. Service @Service(XxxMapper),与 @Resource / Container.get 名称一致
  4. Mapper / Entity 对齐表结构
  5. 重启 npm run dev

对照仓库现有示例

层级User 模块Test 模块
Controllerbusiness/controller/userbusiness/controller/test
Servicebusiness/service/user.tsbusiness/service/test.ts
Mapperbusiness/mapper/user.tsbusiness/mapper/test.ts(含 @Select
Entitybusiness/entity/user.tsbusiness/entity/test.ts

接口说明见 User 接口Test 接口

基于 VitePress 构建