# 在 Func 里查询用户｜Creght AI 编程指南

> 用 ctx.users.find 与 ctx.users.query 在 Creght Func 里读取项目用户目录：用户对象字段、精确查找的 409 与 OAuth 陷阱、列表的搜索/状态/分页/排序参数，以及必须自己实现的权限门禁。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [在服务端调用外部 API 并管理缓存](/api/ssr-external-api-cache.md)
- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [JSON 表：定义、读写与查询](/api/func-json-tables.md)
- [上传文件：直传与 Func 内生成](/api/func-assets-upload.md)
- [超时配置与流式响应](/api/func-timeout-streaming.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)
- [使用集成接入支付宝支付](/api/func-alipay-integration.md)

登录与用户

- [注册时验证邮箱](/api/auth-verified-registration.md)
- [实现找回密码与修改密码](/api/auth-password-reset.md)
- [在 Func 里实现登录](/api/auth-func-login.md)
- [在 Func 里查询用户](/api/func-user-directory.md)

本页目录

- [先分清 ctx.auth 与 ctx.users](#scope)
- [用户对象](#user-object)
- [找一个人：find](#find)
- [查一批人：query](#query)
- [门禁：这一节不能跳过](#gate)
- [用户自定义字段 profile](#profile)
- [什么时候不该用它](#not-this)
- [不要自建用户表](#不要自建用户表)
- [不要拿它做业务查询](#不要拿它做业务查询)
- [不要在页面里做权限](#不要在页面里做权限)
- [不要用它替代登录态](#不要用它替代登录态)
- [验收清单](#checklist)

登录与用户/在 Func 里查询用户

# 在 Func 里查询用户

ctx.users 是项目的用户目录：find 按标识符精确找一个人，query 按条件分页翻名单。含用户对象的完整字段表、find 的三个陷阱、query 的过滤与排序规则，以及最关键的一节——平台不做角色判断，门禁必须由站点代码自己写。

复制 Markdown 链接

站点的用户账号由平台管理，Func 通过 `ctx.users` 读写这份 **项目级用户目录**：按标识符找一个人、按条件查一批人、校验和重设密码、更新用户自定义字段。它和 `ctx.auth` 是两件事——后者只关心"发起这次调用的是谁"。

**智能体目标**

用 `ctx.users` 读用户目录，但每一个用到它的 Func **都必须自己写权限门禁**：平台只保证"这个项目的用户"，不保证"这个调用者有资格看他们"。查询结果不得原样返回浏览器。

## 先分清 ctx.auth 与 ctx.users

这两个命名空间最容易被混着用，而混用的后果不对称：把 `ctx.users` 当成"当前用户"来写，一个填错的邮箱就会改到别人头上。

| 命名空间 | 作用域 | 方法 |
| --- | --- | --- |
| `ctx.auth` | **发起这次调用的人**，来自会话 Cookie | `currentUser()` / `requireUser()` / `login()` / `register()` |
| `ctx.users` | **整个项目的用户目录**，能指到任何一个人 | `find()` / `query()` / `checkPassword()` / `setPassword()` |

> 凡是修改类的调用， **标识符要来自服务端刚刚确认过的事实**，而不是浏览器传上来的字段。改当前用户的密码应该写 `ctx.users.setPassword({ userId: ctx.auth.requireUser().id, ... })`，不是 `{ email: input.email }`——后者等于"谁自称是谁就改谁"。

## 用户对象

`ctx.auth.currentUser()`、 `ctx.users.find()` 和 `ctx.users.query()` 返回的是同一种结构。字段名是 **下划线风格**：

| 字段 | 说明 |
| --- | --- |
| `id` | 用户主键，始终存在。 **这是唯一该用来做归属键的值** |
| `account` / `email` / `phone` | 三种标识符，都可能为空——取决于用户是怎么注册的 |
| `name` / `avatar` | 展示名与头像 URL。没有 `nickname` 字段 |
| `status` | `enabled` 或 `disabled` |
| `profile` | 站点自定义字段，见 [用户自定义字段](#profile) |
| `last_login_at` / `created_at` / `updated_at` | 时间戳 |

对象里 **没有** 密码、会话 token、OAuth 绑定列表和任何内部 ID。这些都不会进入沙箱。

## 找一个人：find

`ctx.users.find(ref)` 按标识符精确定位一个用户， `userId` / `email` / `account` **三个字段只能填一个**，填两个是 400。

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

export function lookup(input, ctx: TalizenFuncContext) {
  const user = ctx.users.find({ email: input.email })
  if (!user) return { ok: false }   // 找不到返回 null，不抛错
  return { ok: true, name: user.name }
}
```

三个陷阱：

- **找不到返回 `null`，不是抛错。**"这个人存不存在"是站点要分支处理的正常结果，不该用 try/catch 包。
- **同一个邮箱可能对应多个用户**，这时 `find` 会抛 409 而不是随便挑一个。允许重复邮箱的项目应该用 `account` 或 `userId` 查。
- **`account` 只能找到有密码身份的用户。** 纯 OAuth 注册的账号没有密码身份行，按 `account` 查不到——用 `email` 或 `userId`。

> 把 `find` 的结果直接暴露成"这个邮箱注册过没有"是一个账号枚举口子。忘记密码、注册这类面向匿名访问者的流程， **两个分支必须返回完全一样的东西**，详见 [找回与修改密码](/api/auth-password-reset.md)。

## 查一批人：query

`ctx.users.query(query)` 按条件分页返回用户，形状与 `ctx.db.query` 一致： `{ total, list, limit }`。

```typescript
export function directory(input, ctx: TalizenFuncContext) {
  requireAdmin(ctx)   // 见下一节，这一行不能省

  const result = ctx.users.query({
    search: input.keyword,        // 模糊匹配 account / email / phone / name
    status: 'enabled',            // 'enabled' | 'disabled'，不传则不过滤
    order_by: 'created_at desc',
    limit: 20,
    offset: (input.page - 1) * 20,
  })

  // 只把页面真正需要的字段交出去
  return {
    total: result.total,
    list: result.list.map((user) => ({
      id: user.id,
      name: user.name,
      createdAt: user.created_at,
    })),
  }
}
```

| 参数 | 说明 |
| --- | --- |
| `search` | 在 `account`、 `email`、 `phone`、 `name` 四个字段上做包含匹配。不传则不过滤 |
| `status` | `enabled` 或 `disabled` |
| `limit` | **默认 20，上限 100**，超出会被截断；返回值里的 `limit` 是实际生效值 |
| `offset` | 分页偏移 |
| `order_by` | 可用 `created_at`、 `last_login_at`、 `id`，默认 `created_at desc` |

**`query` 返回的用户对象不含 `profile`。** 自定义字段可能包含只该给后台看的内容，列表接口一律不带；需要某个人的完整资料时，拿到 `id` 再 `find` 一次。

> 没有聚合、没有分组、没有按 `profile` 字段过滤。要按业务维度筛人（比如"买过课的用户"），把业务事实写进自己的 [JSON 表](/api/func-json-tables.md) 并以 `user.id` 关联，然后查表——用户目录不是业务数据库。

## 门禁：这一节不能跳过

`find` 有一道天然门槛——你得先知道对方的标识符。 **`query` 没有这道门槛**：它把用户目录变成可以整页翻的东西。一个忘了加权限判断的 Func，就是一个公开的客户名单导出接口。

```typescript
// 平台不知道谁是管理员，这个判断只能你自己写
function requireAdmin(ctx: TalizenFuncContext) {
  const user = ctx.auth.requireUser()          // 1. 必须登录
  const admin = ctx.db.get('admins', user.id)  // 2. 必须在你自己的授权表里
  if (!admin) throw new Error('forbidden')
  return user
}
```

- **每一个调用 `query` 的 Func 都要先 `requireUser()`，再判断这个人有没有资格。** 平台只做项目隔离，不做角色判断。
- **不要把 `result.list` 直接返回给浏览器。** 用户对象里有邮箱和手机号，按页面实际需要挑字段再返回。
- 需要"管理员"概念时，用自己的 JSON 表存授权关系，用 `user.id` 关联。不要用邮箱后缀之类的规则硬编码在代码里。
- 用户目录的读取结果不会被缓存，每次都反映库里的真实账号行。

## 用户自定义字段 profile

除了内置字段，每个项目可以给用户定义一组自定义字段，schema 在编辑器的 **后端 → 用户** 里配置。每个字段有两个开关：

| 开关 | 含义 |
| --- | --- |
| `x-customer-readable` | 浏览器能不能读到这个字段（默认能） |
| `x-customer-writable` | 浏览器能不能改这个字段（ **默认不能**） |

这两个开关约束的是 **浏览器**。Func 是服务端代码， `find()` 读到的是 **未经过滤的完整 profile**——包括标了 `x-customer-readable: false` 的字段。所以在 Func 里把 `user.profile` 整个返回给页面，等于绕过了这个开关。

```typescript
export function me(input, ctx: TalizenFuncContext) {
  const user = ctx.users.find({ userId: ctx.auth.requireUser().id })

  // 不要 return { profile: user.profile }：里面可能有只该后台看的字段
  return { plan: user.profile?.plan ?? 'free' }
}
```

**`query()` 返回的用户不带 `profile`**，就是为了让"顺手把列表整个转发出去"不至于变成一次批量泄漏。要某个人的完整资料，拿 `id` 再 `find` 一次——那是一次明确的、针对单个人的读取。

> **Func 里目前不能修改 profile。** 它只在 `ctx.auth.register({ profile })` 建号那一刻可写，之后没有更新入口。所以会变化的用户数据——套餐状态、积分、偏好——应该放进自己的 [JSON 表](/api/func-json-tables.md) 并用 `user.id` 关联，profile 留给注册时就定下来的少量属性。

## 什么时候不该用它

### 不要自建用户表

不要建 `users`、 `auth_users` 这类身份表，也不要用邮箱当业务主键。账号、密码、会话、OAuth 都是平台能力，见 [在 Func 里实现登录](/api/auth-func-login.md)。

### 不要拿它做业务查询

用户目录只能按标识符和这几个内置字段筛选。业务维度的筛选写进自己的 JSON 表，用 `user.id` 关联。

### 不要在页面里做权限

"只有管理员能看到这个列表"必须在 Func 里判断。浏览器侧的隐藏只是显示效果，接口仍然可以被直接调用。

### 不要用它替代登录态

判断"当前是谁"永远用 `ctx.auth`。 `ctx.users.find` 能拿到任何人的对象，它不证明任何登录事实。

## 验收清单

- 每个用到 `ctx.users` 的 Func 都先 `requireUser()`，并对越权访问做了判断。
- 返回给浏览器的是挑选过的字段，不是完整用户对象，也不是完整 `profile`。
- 修改类调用的标识符来自服务端确认过的事实，不是浏览器传入的邮箱。
- 面向匿名访问者的流程，用户存在与否两个分支返回完全一致。
- 列表接口带 `limit` 和 `offset`，按 `total` 翻页。
- 会变化的用户数据在 JSON 表里以 `user.id` 关联，没有指望 `profile`（它在 Func 里只读），也没有另建身份表。

**完成标准**

用户目录的每一次读取都能说清"谁在读、凭什么能读、读到的东西有没有超出页面需要"，并且换一个未授权账号调用同一个 Func 时会被明确拒绝。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

微信客服

## 链接

- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://creghtlib.site.creght.com)
- [动效库](/design/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [自己写代码 vs Creght](/compare/self-coding.md)
- [外包 vs 自己做](/compare/outsourcing.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
