Files
zt-closet-manage-backend/README.md
T
2026-09-14 00:29:16 +08:00

276 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# zt-closet-manage-backend
「智能衣橱」小程序(zt-closet-backend)的**管理后台后端服务**。采用与业务服务一致的 Go + Gin 技术栈与代码风格(四层架构、统一响应、注册式错误码、OpenTelemetry 可观测性),与业务服务**共用同一 MySQL 业务库**,直接读写业务数据表;管理员账号表(`admin_users`)由本服务自行维护。
## 核心功能
- **管理员账号**:账号密码登录(bcrypt 存储)、JWT 双 token(access/refresh)签发与刷新、登出、修改密码;超级管理员可增删改普通管理员、禁用/启用账号;初始超级管理员由配置在启动时幂等创建;禁用/删除账号的会话立即失效。
- **仪表盘统计**:用户总数/今日新增/各状态分布、订单总数/各状态分布、累计与今日营收、累计退款、待处理退款数、待审核穿搭数、上架商品数,一次接口聚合返回。
- **用户管理**:分页搜索(昵称/手机号/OpenID/状态)、详情(含永久额度 + 每日额度 + 额度凭证列表)、封禁/解封(临时按天/永久,写原因与期限)、额度发放(有有效期额度凭证)、额度凭证回收、VIP 人工调整(事务 + 人工调整流水)。
- **商品管理**:分页列表(含下架)、新增、编辑(含 `valid_days` 额度有效期天数)、上下架、软删除;虚拟支付道具 ID 与发布状态只读展示。
- **订单与退款管理**:订单列表/详情(含虚拟支付 pay_channel=2 与 XPay 平台/环境/道具字段);退款申请审核——通过置「退款中」(实际打款由运营在微信商户平台手动完成)、驳回置「失败」并记录原因。
- **内容审核**:公开穿搭列表/详情(含衣物明细)、审核通过/驳回(写 `outfit_audit_logs`)、封禁/解封;衣物列表/详情;衣柜分享列表、封禁/解封。
- **通知推送**:单用户系统通知、全量广播(分批写入 `user_notifications` + 同步 Redis 未读计数,小程序端实时可见)。
## 技术栈
| 分类 | 技术 |
| --- | --- |
| 语言 | Go 1.26 |
| Web 框架 | Gin v1.12 |
| ORM / 数据库 | GORM + MySQL(含 soft_delete 软删除) |
| 缓存 | go-redis v9(管理员 token 会话、通知未读计数) |
| 配置 | spf13/viper(仅读 YAML 文件) |
| 鉴权 | golang-jwt/v5(HS256 双 token)+ bcrypt |
| 可观测性 | OpenTelemetry(otelgin、otelslog、OTLP gRPC 三信号) |
| 日志 | log/slog + lumberjack(JSON、按大小轮转压缩) |
## 目录结构
```
zt-closet-manage-backend/
├── main.go # 入口:初始化配置/OTel/日志/MySQL/Redis,建表+seed 超管,启动 HTTP 服务
├── configs/
│ └── config.example.yaml # 配置模板(含注释说明)
├── global/ # 全局单例:配置指针、DB/Redis 引擎、OTel Tracer/Meter
├── internal/
│ ├── dto/ # 请求/响应 DTO 与分页结构
│ ├── middleware/ # RequestID / OTel / 请求日志(脱敏) / CORS / 管理员 JWT / 超管权限
│ ├── mysqldao/ # MySQL 数据访问层(tracing span 命名 mysqldao.Xxx)
│ ├── mysqlmodel/ # GORM 模型:admin_users(自建)+ 业务表模型镜像
│ ├── redisdao/ # Redis 数据访问层(管理员会话、通知未读计数)
│ ├── redismodel/ # Redis 引擎初始化与 key 常量
│ ├── routers/ # 路由注册(router.go)与各 handler(api/v1)
│ └── service/ # 业务逻辑层(常量 + 各业务域 Service)
└── pkg/
├── app/ # HTTP 统一响应封装 {code, msg, data}
├── errcode/ # 统一业务错误码(公共段 + 管理域分段)
├── lzcwyjwt/ # 管理员 JWT 生成/解析(Claims.AdminID)
├── setting/ # viper 配置读取与配置结构体
└── tracing/ # OpenTelemetry 初始化与日志桥接
```
## 快速开始
### 1. 环境依赖
- Go 1.26+
- MySQL(**与业务服务共用同一个库**,业务表由业务服务的迁移脚本维护)
- Redis(与业务服务共用同一实例与 DB,保证通知未读计数互通)
### 2. 配置准备
```bash
cp configs/config.example.yaml configs/config.yaml
```
编辑 `configs/config.yaml`,关键项:
- `Mysql`:填写**与业务服务相同的数据库**连接信息(`DBName` 一致)。
- `Redis`:填写与业务服务相同的 Redis 连接信息(`DB` 一致,保证未读计数 key 互通)。
- `JWT.SecretKey`:**必填**,须为强随机字符串,且**与业务服务使用不同密钥**(防止跨服务 token 混用)。
- `Admin.InitialUsername` / `Admin.InitialPassword`:初始超级管理员账号,服务启动时幂等创建(已存在则跳过)。
### 3. 启动
```bash
# 必须在项目根目录执行(配置路径固定为相对路径 configs/config.yaml)
go run .
```
启动时自动完成两件事(均在 `main` 中,依赖 MySQL 就绪):
1. **AutoMigrate 仅 `admin_users` 表**(业务表结构由业务服务迁移脚本维护,本服务不做任何业务表 DDL);
2. **幂等 seed 初始超级管理员**(`Admin` 配置段)。
验证健康检查:
```bash
curl http://localhost:8081/ping
# {"message":"pong"}
```
## 配置说明
| 配置段 | 说明 | 关键字段 |
| --- | --- | --- |
| `Server` | HTTP 服务 | `Domain`(CORS 允许域名,`*` 或不填允许所有来源)、`RunMode`、`HttpPort`、`ReadTimeout`/`WriteTimeout` |
| `Log` | 日志 | `LogSavePath`、`LogFileName`、`MaxSize`/`MaxBackups`/`MaxAge`、`LogLevel` |
| `Mysql` | MySQL 连接池 | 用户名/密码/Host/Port/DBName 及连接池参数 |
| `Redis` | Redis 连接 | Host/Port/Password/DB 及连接池、重试、超时参数 |
| `OTel` | OpenTelemetry | `Enabled`、`ServiceName`、`ExporterEndpoint`(OTLP gRPC)、`Insecure`、`SamplerRatio` |
| `JWT` | 鉴权 | `SecretKey`(**必填**)、`Issuer`、`AccessTokenExpire`、`RefreshTokenExpire` |
| `Admin` | 初始超管 | `InitialUsername`、`InitialPassword`(仅首次创建时使用) |
| `Quota` | 免费额度底值 | `Cos`/`AI`/`OutfitShare`/`ClosetShare`(仅用于展示「总额度」,须与业务服务 `UserDefaultQuota` 免费值一致,未配回落 20/3/10/5) |
| `Cos` | CDN 下载 | `CosDownloadDomain`、`CdnDownloadSecretKey`(用于把衣物图片/头像的相对 COS key 拼接为带鉴权签名的可访问 URL,须与业务服务 Cos 段一致) |
## API 概览
- 统一前缀:`/api/v1`
- 统一响应:成功 `{"code":0,"msg":"成功","data":{...}}`;失败 `{"code":<错误码>,"msg":<文案>,"details":[...]}`。判断业务结果一律以 `code` 为准。
- HTTP 状态码:鉴权类错误 401、权限 403、系统错误 500,其余 200。
- 鉴权方式:`Authorization: Bearer <access_token>`(登录接口获取);access 过期(业务码 `11002`)后用 refresh token 换取新 token。
- 分页约定:`page`(≥1)、`page_size`(1~50);响应 `total`/`page`/`page_size`/`list`。
- 金额单位:所有金额字段(`price`、`discount_price`、`pay_amount`、`refund_amount` 等)均为**分**(int64)。
### 公开路由(无需登录)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/ping` | 健康检查 |
| POST | `/admin/login` | 管理员登录 |
| POST | `/admin/refresh-token` | 刷新 token(需旧 access + refresh) |
### 鉴权路由(需登录)
**管理员(自己)**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/admin/logout` | 登出 |
| PUT | `/admin/password` | 修改自己密码 |
**仪表盘**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/dashboard/summary` | 核心指标聚合 |
**用户管理**(路径参数 `:uuid` 为用户 UUID)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/users` | 用户列表(query:page/page_size/keyword/status) |
| GET | `/users/:uuid` | 用户详情(含永久额度 + 每日额度 + 额度凭证列表 `grants`) |
| PUT | `/users/:uuid/ban` | 封禁(json:ban_type 1临时/2永久、ban_days、reason) |
| PUT | `/users/:uuid/unban` | 解封 |
| PUT | `/users/:uuid/quota` | 额度发放(json:quota_type 1上传/2AI/3社区/4衣柜、amount、expire_days) |
| PUT | `/users/:uuid/vip` | VIP 人工调整(json:vip_type、vip_days、reason) |
| DELETE | `/users/:uuid/grants/:id` | 回收额度凭证(软删) |
**商品管理**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/goods` | 商品列表(query:page/page_size/goods_type/status/keyword) |
| POST | `/goods` | 创建商品 |
| PUT | `/goods/:id` | 更新商品(传什么改什么) |
| PUT | `/goods/:id/status` | 上下架(json:status 1上架/2下架) |
| DELETE | `/goods/:id` | 软删除商品 |
**订单与退款**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/orders` | 订单列表(query:page/page_size/order_no/user_id/status) |
| GET | `/orders/:orderNo` | 订单详情 |
| GET | `/refunds` | 退款单列表(query:page/page_size/status/order_no) |
| POST | `/refunds/:refundNo/approve` | 审核通过(待处理→退款中) |
| POST | `/refunds/:refundNo/reject` | 审核驳回(待处理→失败,写原因) |
**内容审核**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/outfits` | 穿搭列表(query:page/page_size/user_id/is_public/audit_status/status/keyword) |
| GET | `/outfits/:uuid` | 穿搭详情(含衣物明细) |
| POST | `/outfits/:uuid/audit` | 审核(json:result 1通过/2驳回、reason) |
| POST | `/outfits/:uuid/ban` | 封禁穿搭 |
| POST | `/outfits/:uuid/unban` | 解封穿搭 |
| GET | `/clothes` | 衣物列表(query:page/page_size/user_id/image_process_status/category_main/keyword) |
| GET | `/clothes/:uuid` | 衣物详情 |
| GET | `/closet-shares` | 衣柜分享列表(query:page/page_size/user_id/status) |
| POST | `/closet-shares/:uuid/ban` | 封禁分享 |
| POST | `/closet-shares/:uuid/unban` | 解封分享 |
**通知推送**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/notifications` | 单用户通知(json:user_id、title、content) |
| POST | `/notifications/broadcast` | 全量广播(json:title、content;响应 sent_count) |
### 超级管理员专属路由
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/admins` | 管理员列表 |
| POST | `/admins` | 创建管理员 |
| PUT | `/admins/:id` | 更新管理员(昵称/密码/角色/状态) |
| DELETE | `/admins/:id` | 删除管理员(软删) |
## 关键业务规则
- **退款审核状态机**:仅允许 `0 待处理 → 1 退款中`(通过)与 `0 待处理 → 3 失败`(驳回)。实际微信打款由运营在微信商户平台手动完成,业务服务的退款回调负责后续「成功(2)」流转与权益回收。
- **额度体系**(与业务服务一致):`user_quotas` 仅存 Cos 永久额度;AI/社区/衣柜 为每日额度;有有效期赠送额度走 `quota_grants` 凭证(source:1 会员包 / 2 加量包 / 3 广告激励)。管理后台「额度发放」写入 `quota_grants`(source=2 加量包、`ref_id=manual_<uuid>`、`expire_time=now+expire_days*86400`):Cos 为一次性库存,AI/社区/衣柜 为有效期内每日加量;「凭证回收」软删除对应凭证。有效额度 = 免费配置值(`Quota` 段)+ 有效凭证,每日已用读 `user_daily_usages`。
- **VIP 人工调整**:叠加语义(`vip_expire = max(当前到期, now) + vip_days*86400`),`vip_days=0` 表示取消会员;事务内更新 `users` + 写 `vip_flow_logs`(`change_type=5`、`source=4`)。
- **用户封禁**:改 `users.status`(2 临时/3 永久)+ 原因与期限,业务服务鉴权中间件每次请求校验状态,**改库即生效**。
- **穿搭可见性**:`is_public=2` 且 `audit_status=2` 且 `status=1` 三者同时满足才对外可见;审核驳回置 `audit_status=3`、封禁置 `status=2`。
- **通知未读计数**:管理后台自增业务服务的同一 Redis key(`ztcloset:notification:unread:<user_id>`),要求两者共用同一 Redis 实例与 DB。
- **管理员会话**:access/refresh 双 token 会话写 Redis;禁用/删除管理员会失效其全部会话;JWTAuth 每次请求回查数据库状态,禁用立即生效(Redis 故障时 fail-open,但 DB 回查兜底拦截禁用账号)。
## 错误码
公共段与业务服务一致(0 成功、10000 参数、11000 鉴权、12000 权限、19000 系统);管理域独立分段:
| 段位 | 业务域 |
| --- | --- |
| 15000–15999 | 管理员(登录/账号) |
| 16000–16999 | 用户管理 |
| 17000–17999 | 商品 |
| 18000–18999 | 订单/退款 |
| 20000–20999 | 内容审核(穿搭/衣物/分享) |
| 21000–21999 | 通知 |
## 安全注意事项
- `JWT.SecretKey` 必须为强随机值且**与业务服务不同**,留空会导致启动失败;请勿使用默认占位值。
- `Cos.CdnDownloadSecretKey` 为 CDN 下载鉴权密钥(机密),仅存 `configs/config.yaml`(已被 `.gitignore` 忽略),且须与业务服务 `Cos` 段一致。
- 管理员密码一律 bcrypt 哈希入库;初始密码仅存在于 `configs/config.yaml`(已被 `.gitignore` 忽略),首次登录后请立即修改。
- 所有 SQL 走 GORM 参数绑定;请求日志对 `token`/`password` 等敏感参数自动脱敏。
- 登录接口对「账号不存在」与「密码错误」返回同一错误码,避免账号枚举攻击。
- 生产环境请将 `Server.RunMode` 设为 `release`,并通过反向代理对外提供 HTTPS。
## CI/CD 与部署
### 流水线(Gitea Actions)
`.gitea/workflows/deploy.yaml` 提供「构建并部署」流水线:
| 触发方式 | 说明 |
| --- | --- |
| 推送 `v*` 标签 | 如 `git tag v1.0.0 && git push origin v1.0.0` |
| 手动触发 | Gitea 仓库页面 Actions 标签页 → 选择「构建并部署」→ Run workflow |
流程:拉取代码 → 构建镜像(多阶段 Dockerfile,静态编译)→ 推送 `git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend`(tag + latest)→ SSH 部署生产机 → `/ping` 健康检查;**检查失败自动输出日志并回滚到旧镜像**。流程结束后通过钉钉机器人推送**成功/失败通知**(含仓库、分支/标签、提交、触发者、镜像、耗时与运行详情链接)。
> 管理后台无数据库迁移命令(`admin_users` 表由服务启动时 GORM AutoMigrate 幂等创建),因此没有业务服务那样的迁移流水线。
### Gitea 仓库 Secrets / Variables(需自行创建)
| 类型 | 名称 | 说明 |
| --- | --- | --- |
| Secret | `ZT_CLOSET_REGISTRY_USER` | Gitea 镜像仓库登录用户名 |
| Secret | `ZT_CLOSET_REGISTRY_TOKEN` | Gitea 镜像仓库登录令牌 |
| Secret | `ZT_CLOSET_PROD_SSH_KEY` | 生产机部署 SSH 私钥 |
| Secret | `ZT_CLOSET_DINGTALK_WEBHOOK` | 钉钉机器人 Webhook 完整地址(构建部署成功/失败通知;未配置则自动跳过通知) |
| Variable | `ZT_CLOSET_PROD_SSH_HOST` | 生产机 IP/域名 |
| Variable | `ZT_CLOSET_PROD_SSH_USER` | 生产机 SSH 用户 |
> 以上名称与业务服务 `zt-closet-backend` **完全一致**(两个服务部署在同一台生产机,镜像仓库与钉钉机器人也共用)。建议建在**组织级**,两个仓库自动继承,无需重复配置;若建在仓库级,则需在本仓库再建一份同名同值的条目。
> 注意:因名称相同,`ZT_CLOSET_PROD_SSH_HOST` 的值必须同时适用于两个服务;若将来管理后台部署到独立机器,需改回独立前缀(如 `ZT_CLOSET_MANAGE_*`)。
### 生产机准备
- 部署目录:`/opt/zt-closet-manage`(流水线会自动创建 `configs/` 与 `storage/logs/`)。
- 手动准备 `configs/config.yaml`(见上文配置说明);其 `Server.HttpPort` 必须为 `8081`(与流水线健康检查端口一致)。
- 容器以非 root 用户(uid 10001)运行,日志目录权限由流水线自动设置。
- 本地构建验证:`docker build --build-arg GOPROXY=https://goproxy.cn,direct -f scripts/dockerfile/Dockerfile -t zt-closet-manage-backend:test .`
## 部署提示
- **工作目录**:启动时必须位于项目根目录(配置路径固定为 `configs/config.yaml`)。
- **与业务服务的关系**:共用 MySQL 业务库与 Redis;业务表结构变更由业务服务迁移脚本负责,本服务只维护 `admin_users` 表,不做任何业务表 DDL。
- **多实例**:支持水平扩展;写操作通过事务与条件更新保证幂等(退款审核条件流转、额度调整行锁)。
- **可观测性后端**:按需配置 `OTel.ExporterEndpoint` 指向 OTLP Collector;无需可观测性时设置 `Enabled: false` 即可零开销关闭。