This commit is contained in:
Your Name
2026-09-03 08:38:17 +08:00
parent 6cd4f1b1db
commit 842990b0e7
1853 changed files with 278406 additions and 361 deletions
@@ -0,0 +1,26 @@
# 管理端新增客户端用户
入口:管理端 → 用户管理 → 新增用户。必填中国大陆手机号、昵称和初始密码;性别、城市、简介可选。创建后列表自动定位到新用户,可继续编辑资料。
- 账号默认为正常、正式用户、普通会员状态、未认证;创建时不发短信、不创建登录会话。
- 初始密码必填,但不限制长度或字符组合,纯数字、纯字母、中文等均可。账号可在 App/H5 使用手机号和初始密码登录;请通过安全渠道交付密码,并提醒用户登录后修改。
- 手机号通过唯一索引防止重复(包括已停用或尚未释放手机号的账号),使用 AES-GCM 加密保存;密码以 bcrypt 哈希保存,超过 72 字节的密码使用 Argon2id,兼容已有账号。通用 HTTP 请求大小限制仍然生效。
- 账号、资料、隐私设置、通知设置和审计日志在同一事务内创建;任一写入失败会回滚。
## 接口与权限
`POST /admin/v1/users`,必须携带有效管理员令牌,并具备 `users:create` 权限。请求字段:`phone``password``nickname`,可选 `gender`0/1/2)、`city``bio`。不接受设置会员、认证、测试标记或账号状态的额外字段。
成功返回通用响应中的 `data: { id, publicId }`,不返回密码或用户令牌。参数错误返回 HTTP 400,手机号重复返回 409,权限不足返回 403,数据库错误返回 500。
审计动作是 `create_user`,包含操作管理员、目标用户、星遇号、昵称、来源及操作 IP,不保存初始密码或完整手机号。
## 发布
1. 使用项目现有迁移流程执行 `backend/migrations/029_admin_create_users.sql`(此前的迁移必须已执行)。该迁移新增权限,并仅默认授予内置超级管理员角色。
2. 发布更新后的后端和管理端构建。管理端构建命令:在 `admin` 目录执行 `pnpm --filter @vben/web-ele build`
3. 管理员重新登录以刷新权限;其他运营角色可在“管理员与权限”中按需勾选“新增客户端用户”,并保留“查看用户”以访问用户列表。
## 验证
`go test ./internal/app -run TestAdminCreateUser -count=1` 覆盖真实路由的认证和权限、参数校验、重复手机号保护、敏感数据保护、事务回滚、列表查询及密码登录。测试使用内存 SQL 适配器,不接触线上账号;真实数据库迁移和部署后仍需验收。
@@ -0,0 +1,51 @@
# 头像上传与缩略图
头像专用上传在服务端生成静态图片,不需要开通 COS、OSS 等厂商的图片处理功能,也不改变管理员选择的存储厂商。当前规则只应用于新上传的头像,不下载、覆盖或批量替换已有用户头像。
## 接口
`POST /api/v1/media/upload`,使用原有用户认证,`multipart/form-data`
- `file`:头像文件。
- `purpose=avatar`:启用头像处理。不传此字段时,普通图片、封面、语音和证据上传仍原样保存。
成功响应的 `data` 保留已有字段,同时提供两个可选地址:
```json
{
"id": 42,
"name": "42-1788189400000-av1.jpg",
"url": "https://cdn.example.com/media/2026/08/42-1788189400000-av1.jpg",
"thumbnailUrl": "https://cdn.example.com/media/2026/08/42-1788189400000-av1-thumb.jpg",
"originalUrl": "https://cdn.example.com/media/2026/08/42-1788189400000-av1-original.png",
"provider": "tencent_cos",
"objectKey": "media/2026/08/42-1788189400000-av1.jpg"
}
```
- `url`:最长边不超过 **640px** 的 JPEG 展示图,质量 82。将此 URL 保存到用户资料的 `avatar` 字段。旧客户端和管理端也能正常显示这张较小的图片。
- `thumbnailUrl`:最长边不超过 **256px** 的 JPEG 缩略图,质量 78。uni-app 的列表、聊天、评论、资料头像等小尺寸位置加载此图;首页推荐卡片使用 640px 展示图。
- `originalUrl`:本次上传的原始文件副本,字节不变;若客户端选择图片时已压缩,则这里保留的是压缩后实际上传的文件,并非相册原始文件。
- 未传 `purpose` 的普通媒体上传,`thumbnailUrl``originalUrl` 为空字符串,`url` 仍为原文件。
图片等比例缩小,不放大、不在服务器裁剪人脸。客户端继续通过 `aspectFill` 显示。JPEG 手机照片先按 EXIF 方向校正,再缩放;展示图和缩略图不含原 EXIF/GPS。透明区域合成白底。GIF 使用首帧,WebP 使用解码器支持的静态图片;不支持的动画 WebP 或损坏文件返回明确错误,不悄悄退回高清原图。
## 可靠性与兼容性
- 延用 16 MiB 文件限制,头像另限制为最多 2400 万像素、单边最多 16384px,并限制同时解码的数量,避免小服务器被大图耗尽内存。处理繁忙时返回 503 和 `Retry-After: 2`
- 原图、缩略图和展示图全部上传成功后,才激活媒体记录、返回成功。中途失败或数据库收尾失败会尝试删除已写入的对象和未完成的记录;清理使用独立超时,不受客户端取消影响。本地同名文件不会被覆盖或误删。
- `media_assets` 记录主展示图及其实际 MIME/大小,不需要数据库迁移;原图和缩略图使用同一前缀的配套文件。以后实现物理删除或存储生命周期规则时,需要同时处理三种文件。
- `-av1` 文件名是已生成配套缩略图的标识。客户端只改写这些普通公开 URL,旧头像、第三方 OAuth 头像、临时本地文件和带查询参数的签名 URL 保持原样。缩略图失败时仅回退一次到展示图,防止重复请求。
- 头像组件附带 `lazy-load`,由支持此属性的平台延迟加载;主要性能收益来自真正减小图片像素和文件体积,不依赖所有端都支持懒加载。
- 编辑资料时禁止上传未结束就保存,避免把旧头像误存回服务器。令牌刷新重试会保留 `purpose=avatar`
- 旧客户端继续调用普通上传接口时行为不变。需要新版客户端的头像上传调用,才会触发生成;已存在的头像不会自动变小,需要重新上传或另行安排有备份的批量转换。
## 验证与发布
运行 `go test ./...``pnpm test:avatars``pnpm test:native-components``pnpm type-check`。回归覆盖尺寸、透明图、方向信息、GIF/WebP、损坏或超大图片、存储失败清理、本地同名文件保护、实际 HTTP 上传/读取,以及前端回退和令牌刷新。
项目里的一个头像样本从 1,853,171 字节生成 46,180 字节展示图与 11,131 字节缩略图,缩略图缩小约 99.4%;比例取决于原图内容和格式,不能视作所有图片的固定压缩率。
上线需要更新后端和 H5,原生 App 需重新构建/安装。这份改动本身不执行生产部署,不改线上账号或历史文件。旧后端不认识新增的本地 `-av1` 文件名;如果使用本地存储,回滚服务时必须保留新的静态文件名规则,或由静态服务器继续提供这些文件。
实现参考:[imaging 缩放和 EXIF 方向处理](https://github.com/disintegration/imaging)、[uni-app image 平台属性](https://uniapp.dcloud.net.cn/component/image.html)。
@@ -0,0 +1,17 @@
# 密码输入规则
管理端修改密码、管理员创建/重置密码、管理端新增客户端用户,以及客户端注册、找回和修改密码统一取消长度和字符组合要求。允许单个字符、纯数字、纯字母、中文等,不展示密码强度条,也不限制新密码必须不同于旧密码。
保留密码必填、两次输入一致、旧密码核验,以及登录鉴权、短信验证码、接口权限和请求频率限制。修改密码后原有会话继续失效。编辑管理员资料时密码留空仍表示不修改密码。
## 存储兼容
原有 bcrypt 密码不需要迁移。72 字节以内的密码继续使用 bcrypt;更长的密码使用带随机盐的 Argon2id(19 MiB 内存、2 次迭代、1 个线程),不截断输入。验证时根据哈希前缀选择算法,只接受已支持的参数,避免损坏数据触发不受控的内存分配。已有 `password_hash VARCHAR(255)` 字段可容纳这两种格式。
使用 Argon2id 是为了避免移除输入上限后触发 bcrypt 的技术限制,参数参考 [OWASP 密码存储建议](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html),实现使用 [Go Argon2 包](https://pkg.go.dev/golang.org/x/crypto/argon2)。通用 HTTP 请求体积限制保留,不属于密码格式规则。
## 验证与发布
后端测试覆盖简单密码及长密码的创建和登录、管理员修改密码、旧密码验证、旧会话撤销失败时回滚、已有 bcrypt 密码兼容、长密码尾部不同不能登录、随机盐和损坏哈希处理。
本次修改无需新增数据库迁移。需同步发布后端和管理端;App 用户需安装新包,H5 更新构建。长密码一旦保存为 Argon2id,回退后端到旧版本会导致这些账号无法登录,回退时应保留新哈希的验证支持。
@@ -0,0 +1,182 @@
syntax = "v1"
info (
title: "星遇社交平台 API"
desc: "账户、发现、动态、会员与 IM REST 接口定义"
author: "Xingyu"
version: "1.0"
)
type LoginRequest { Phone string `json:"phone"`; Password string `json:"password"`; DeviceID string `json:"deviceId"` }
type LoginResponse { AccessToken string `json:"accessToken"`; RefreshToken string `json:"refreshToken"`; ExpiresIn int64 `json:"expiresIn"`; UserID int64 `json:"userId"` }
type CreatePostRequest { Content string `json:"content"`; Media []string `json:"media"`; Location string `json:"location"`; Visibility int `json:"visibility"` }
type SMSRequest { Phone string `json:"phone"`; Scene string `json:"scene"` }
type ResetPasswordRequest { Phone string `json:"phone"`; Code string `json:"code"`; Password string `json:"password"` }
type SendMessageRequest { ClientMsgID string `json:"clientMsgId"`; Type int `json:"type"`; Content map[string]string `json:"content"` }
type UserVerificationRequest { Type string `json:"type"`; RealName string `json:"realName"`; DocumentNumber string `json:"documentNumber"`; Evidence []string `json:"evidence"` }
type AdminProfileRequest { Phone string `json:"phone"`; Nickname string `json:"nickname"`; Avatar string `json:"avatar"`; Cover string `json:"cover"`; Gender int `json:"gender"`; Birthday string `json:"birthday"`; Height int `json:"height"`; CityCode string `json:"cityCode"`; City string `json:"city"`; Occupation string `json:"occupation"`; Bio string `json:"bio"` }
type VerificationRequest { Type string `json:"type"`; Status string `json:"status"`; RealName string `json:"realName"`; DocumentMask string `json:"documentMask"`; Remark string `json:"remark"` }
type MembershipGrantRequest { Operation string `json:"operation"`; PlanID int64 `json:"planId"`; ExpiresAt string `json:"expiresAt"`; Reason string `json:"reason"` }
type PasswordResetByAdminRequest { NewPassword string `json:"newPassword"` }
type AdminCreateUserRequest { Phone string `json:"phone"`; Password string `json:"password"`; Nickname string `json:"nickname"`; Gender int `json:"gender,optional"`; City string `json:"city,optional"`; Bio string `json:"bio,optional"` }
type SanctionRequest { Type string `json:"type"`; Reason string `json:"reason"`; ExpiresAt string `json:"expiresAt"` }
type MembershipPlanRequest { Code string `json:"code"`; Name string `json:"name"`; Level int `json:"level"`; DurationDays int `json:"durationDays"`; DailyActiveChatLimit int `json:"dailyActiveChatLimit"`; DailyLikeLimit int `json:"dailyLikeLimit"`; CanViewVisitors bool `json:"canViewVisitors"`; CanInvisibleVisit bool `json:"canInvisibleVisit"`; RecommendationWeight int `json:"recommendationWeight"`; PriceCent int `json:"priceCent"`; OriginalPriceCent int `json:"originalPriceCent"`; Status int `json:"status"`; SortOrder int `json:"sortOrder"` }
type AdminOrderUpdateRequest { ProductID int64 `json:"productId"`; AmountCent int `json:"amountCent"`; Channel string `json:"channel"`; Status string `json:"status"` }
type AdminAccountRequest { Username string `json:"username"`; DisplayName string `json:"displayName"`; Password string `json:"password"`; Status int `json:"status"`; RoleIDs []int64 `json:"roleIds"` }
type AdminRoleRequest { Code string `json:"code"`; Name string `json:"name"`; Description string `json:"description"`; Status int `json:"status"`; Permissions []string `json:"permissions"` }
type MessageModerationRequest { Action string `json:"action"`; Reason string `json:"reason"` }
type FeedbackHandleRequest { Status string `json:"status"`; Reply string `json:"reply"` }
type AccountClosureHandleRequest { Action string `json:"action"`; Reason string `json:"reason"` }
type AppVersionRequest { Platform string `json:"platform"`; Version string `json:"version"`; BuildNumber int `json:"buildNumber"`; ForceUpdate bool `json:"forceUpdate"`; DownloadURL string `json:"downloadUrl"`; ReleaseNotes string `json:"releaseNotes"`; Status int `json:"status"` }
@server (prefix: /api/v1)
service social-api {
@handler Login
post /auth/login/password (LoginRequest) returns (LoginResponse)
@handler SendSMS
post /auth/sms/send (SMSRequest)
@handler ResetPassword
post /auth/password/reset (ResetPasswordRequest)
@handler Me
get /me
@handler MyVerification
get /me/verification
@handler SubmitVerification
post /me/verification (UserVerificationRequest)
@handler SearchUsers
get /users/search
@handler Following
get /me/following
@handler Followers
get /me/followers
@handler Visitors
get /me/visitors
@handler Privacy
get /me/privacy
@handler NotificationSettings
get /me/notification-settings
@handler UpdateNotificationSettings
put /me/notification-settings
@handler MyDevices
get /me/devices
@handler MyFeedback
get /me/feedback
@handler SubmitFeedback
post /me/feedback
@handler AccountClosure
get /me/account-closure
@handler MyOrders
get /me/orders
@handler MyReports
get /me/reports
@handler Discover
get /discover/recommendations
@handler Nearby
get /nearby/users
@handler Feed
get /feed
@handler CreatePost
post /posts (CreatePostRequest)
@handler PostDetail
get /posts/:id
@handler Comments
get /posts/:id/comments
@handler Conversations
get /im/conversations
@handler Messages
get /im/conversations/:id/messages
@handler SendMessage
post /im/conversations/:id/messages (SendMessageRequest)
@handler RecallMessage
post /im/messages/:id/recall
@handler MembershipPlans
get /membership/plans
@handler PaymentChannels
get /payment/channels
@handler CreateOrder
post /orders
@handler PayOrder
post /orders/:id/pay
}
@server (prefix: /admin/v1)
service social-admin-api {
@handler AdminCreateUser
post /users (AdminCreateUserRequest)
@handler AdminUsers
get /users
@handler AdminUserDetail
get /users/:id
@handler AdminUpdateProfile
put /users/:id/profile (AdminProfileRequest)
@handler AdminUpdateVerification
put /users/:id/verification (VerificationRequest)
@handler AdminUpdateMembership
put /users/:id/membership (MembershipGrantRequest)
@handler AdminResetPassword
post /users/:id/password-reset (PasswordResetByAdminRequest)
@handler AdminForceLogout
post /users/:id/force-logout
@handler AdminUserSanctions
get /users/:id/sanctions
@handler AdminCreateSanction
post /users/:id/sanctions (SanctionRequest)
@handler AdminRevokeSanction
post /sanctions/:id/revoke
@handler AdminOrders
get /orders
@handler AdminUpdateOrder
put /orders/:id (AdminOrderUpdateRequest)
@handler AdminDeleteOrder
delete /orders/:id
@handler AdminMarkOrderPaid
post /orders/:id/pay
@handler AdminCloseOrder
post /orders/:id/close
@handler AdminRefundOrder
post /orders/:id/refund
@handler AdminMembershipPlans
get /membership/plans
@handler AdminCreateMembershipPlan
post /membership/plans (MembershipPlanRequest)
@handler AdminUpdateMembershipPlan
put /membership/plans/:id (MembershipPlanRequest)
@handler AdminDeleteMembershipPlan
delete /membership/plans/:id
@handler AdminMessages
get /messages
@handler AdminModerateMessage
post /messages/:id/moderate (MessageModerationRequest)
@handler AdminFeedback
get /client-feedback
@handler AdminHandleFeedback
put /client-feedback/:id (FeedbackHandleRequest)
@handler AdminAccountClosures
get /account-closures
@handler AdminCancelAccountClosure
put /account-closures/:id (AccountClosureHandleRequest)
@handler AdminAppVersions
get /app-versions
@handler AdminCreateAppVersion
post /app-versions (AppVersionRequest)
@handler AdminUpdateAppVersion
put /app-versions/:id (AppVersionRequest)
@handler AdminDeleteAppVersion
delete /app-versions/:id
@handler AdminAccounts
get /admin-users
@handler AdminCreateAccount
post /admin-users (AdminAccountRequest)
@handler AdminUpdateAccount
put /admin-users/:id (AdminAccountRequest)
@handler AdminRoles
get /admin-roles
@handler AdminCreateRole
post /admin-roles (AdminRoleRequest)
@handler AdminUpdateRole
put /admin-roles/:id (AdminRoleRequest)
@handler AdminDeleteRole
delete /admin-roles/:id
@handler AdminPermissions
get /admin-permissions
}