怡途Claw 用户资料、密码、头像修改与账户注销 API 实战:注销不是一刀切 DELETE,而是软删除混合策略--余额、订阅、未完成支付先拦截,隐私数据硬删、财务数据留审计、手机号释放可重注册。
前面讲了登录拿 token。拿到 token 之后,/api/app/* 这组接口里有一类是专门管"我自己"的:看资料、改资料、改密码、换头像,以及最麻烦的--注销账号。
实际接进来会碰到几个问题:
- 改昵称到底调哪个接口?
PUT /profile里的name和nickname填哪个? - 改密码要带旧密码吗,新密码有什么限制?
- 用户要注销账号,直接
DELETE就完了?余额和订阅怎么办? - 注销之后手机号还能不能重新注册?聊天记录真的删干净了吗?
- 注销时为什么要输密码,还让我手打"注销"两个字?
这组接口挂在 app/users 下,全部要带 Authorization: Bearer <token>。本文从 app-users.controller.ts 和 account-deletion.service.ts 的代码来看每个接口到底做了什么。
看资料:GET /api/app/users/profile
最简单的一个,返回当前登录用户的资料:
curl https://www.yitukeji.cn/api/app/users/profile \
-H "Authorization: Bearer <你的token>"
返回:
{
"success": true,
"data": {
"id": 101,
"phone": "13800138000",
"name": "张三",
"nickname": "张三",
"email": "zhangsan@example.com",
"avatar": "https://...",
"role": "user",
"createdAt": "2026-07-01T10:00:00.000Z",
"subscription": { "...": "当前订阅" },
"package": { "...": "套餐信息" },
"agent": { "...": "该用户的 agent 概要" }
}
}
几个点:
name和nickname返回的是同一个值(都是user.name),后端没单独存 nickname,nickname字段是为了兼容老前端保留的。subscription、package、agent是三个聚合字段,findByIdWithSubscription一次查出来带回来。其中package兜底取subscription,所以没订阅时package也可能是null。- 和
GET /api/app/auth/me返回内容基本一致,区别只是语义上一个偏"我是谁"一个偏"我的资料",按需选一个调就行。
改资料:PUT /api/app/users/profile
请求体:
{
"nickname": "新昵称",
"email": "new@example.com",
"avatar": "https://..."
}
三个字段都是可选的,传哪个改哪个。email 走 @IsEmail 校验,格式不对直接 400 邮箱格式不正确。
一个容易踩的细节:DTO 里同时有 name 和 nickname 两个字段,后端取值是 dto.nickname ?? dto.name--nickname 优先,nickname 没传才回退到 name。所以你想改昵称,传 nickname 就行,别两个都传搞混。
改完返回更新后的资料,结构和 GET /profile 一样。
换头像:PUT /api/app/users/avatar
单独拎出来一个接口,因为头像经常单独传。请求体就一个字段:
{ "avatar": "https://cdn.example.com/avatar.png" }
avatar 是字符串 URL,非空,最长 500 字符,超了返回 头像URL长度不能超过500字符。
注意这个接口只存 URL,不接文件上传。图片本身要先走 /api/app/files/* 或 /api/agent/og-save 传到七牛拿到外链,再把 URL 提交到这里。和 304 那篇讲的文件上传是两段链路。
改密码:PUT /api/app/users/password
请求体:
{
"currentPassword": "旧密码",
"newPassword": "新密码"
}
两个字段都必填,都至少 6 位(DTO 层 @MinLength(6)),少了返回 当前密码至少6位 / 新密码至少6位。
后端会先 bcrypt.compare 验旧密码对不对,错了返回 密码错误。新密码会和旧密码做同样的 hash 存回去。返回就一句 密码修改成功,不带数据。
改完密码不需要重新登录--token 还在 24 小时有效期内,不会因为改密码失效。这点和下面注销不一样。
注销前置检查:GET /api/app/users/account/deletion-check
注销账号不是 DELETE 一刀切。先调这个接口预览一下当前账户能不能注销、有什么拦着:
curl https://www.yitukeji.cn/api/app/users/account/deletion-check \
-H "Authorization: Bearer <你的token>"
返回:
{
"canDelete": false,
"blockers": [
{
"code": "BALANCE_NOT_ZERO",
"message": "账户余额剩余 12.50 元,请先联系客服处理或消费完再注销",
"detail": { "balance": 12.5 }
},
{
"code": "ACTIVE_SUBSCRIPTION",
"message": "有 1 个未到期订阅,请先取消订阅或等待订阅到期",
"detail": { "count": 1 }
}
],
"summary": {
"balance": 12.5,
"pendingPaymentCount": 0,
"activeSubscriptionCount": 1,
"activeInstanceCount": 2
}
}
四类拦截(任一命中就 canDelete: false):
- 余额大于 0(
BALANCE_NOT_ZERO):账户里还有钱,得先消费完或联系客服退,不能带着钱销户。 - 有未完成支付(
PENDING_PAYMENT):payment_orders里有pending状态的订单,得先取消或等它完成。 - 有未到期订阅(
ACTIVE_SUBSCRIPTION):active或trial且currentPeriodEnd > now的订阅。注意这里特意排除了"active 但已过期"的,过期的不拦。 - 运行中实例不拦截,只是统计在
summary.activeInstanceCount里--注销时会自动forceDelete掉,不用你手动停。
blockers 数组把所有拦路项一次列全,summary 给数量概览。前端可以直接拿 blockers 渲染提示,告诉用户先处理哪几样。
执行注销:DELETE /api/app/users/account
确认 canDelete: true 之后才能调。这个接口有两道确认:
curl -X DELETE https://www.yitukeji.cn/api/app/users/account \
-H "Authorization: Bearer <你的token>" \
-H "Content-Type: application/json" \
-d '{ "password": "你的密码", "reason": "不用了", "confirmText": "注销" }'
两道保险:
password必填,后端bcrypt.compare验密码对不对,错了返回密码错误。这是防止 token 泄露后被人恶意注销。confirmText如果传了,必须正好等于字符串注销(两个字),否则返回确认文本不匹配,请输入"注销"两个字。这是防误操作的第二道关,前端通常做成让用户手打"注销"才能点确认按钮。reason可选,注销原因,会写进审计记录。
deletedBy 固定是 self(用户自助注销)。后台 admin 也能强制注销,那时 deletedBy 是 admin:<adminId>,可以跳过前置检查和密码确认。
注销到底删了什么
这是整个接口里最值得讲的部分。注销不是一条 DELETE FROM users,而是"软删除混合"策略,分三类数据区别对待:
用户主表 users:软删
isActive置false--这就是 307 那篇讲的"注销即失效"的开关,老 token 下次请求查到isActive=false直接拒绝。phone改名成deleted_<8位uuid>_<原手机号>,截断到 250 字符。这样原手机号的 unique 约束被释放,将来同号能重新注册。email置null,同样释放 unique。password打乱成DELETED_<uuid>,防止 admin 冒登。name保留原值,方便 admin 后台展示"这个号以前是谁"。
隐私类数据:硬删
14 张表按 user_id 物理删除,包括 messages(聊天记录)、api_keys、user_devices、instance_files、token_usage_records、user_balances、recharge_records、login_logs、chat_run_events 等。这些是真删,不留。
财务类数据:保留
payment_orders、apple_iap_purchases、billing_records 不动,userId 原样保留。这是给税务和合规审计留的,展示层靠 isActive=false 过滤掉注销用户即可。
运行中的实例:先 forceDelete 再删库
Docker 容器和 DB 记录在事务外先 forceDelete 掉(涉及 docker 副作用,不可回滚),单个实例删失败不阻塞注销,只 warn 记审计。然后才进事务做上面三类数据清理。
整个过程在 account_deletion_records 表留一条审计快照:手机号、邮箱、注销前余额、实例数、订阅快照、操作人、客户端 IP、User-Agent、注销原因。admin 后台能回溯每一次注销。
一条串起来的链路
把 307 和这篇放一起看,注销的安全设计是闭环的:
- 注销时
isActive=false+password打乱 - 老 token 还没过期,但下次调任意
/api/app/*接口,JwtAuthGuard校验时查库发现isActive=false,直接返回账户已注销 - 不用等 token 自然过期,注销即时生效
- 手机号释放,将来能重新注册新账号
代价是每个鉴权请求多一次 DB 查询。代码注释写得很直白:注销能即时生效比省这一次查询重要。对一个按 token 计费、可能存客户数据的平台,这个取舍合理。