# zt-closet-manage-backend 「智能衣橱」小程序(zt-closet-backend)的**管理后台后端服务**。采用与业务服务一致的 Go + Gin 技术栈与代码风格(四层架构、统一响应、注册式错误码、OpenTelemetry 可观测性),与业务服务**共用同一 MySQL 业务库**,直接读写业务数据表;管理员账号表(`admin_users`)由本服务自行维护。 ## 核心功能 - **管理员账号**:账号密码登录(bcrypt 存储)、JWT 双 token(access/refresh)签发与刷新、登出、修改密码;超级管理员可增删改普通管理员、禁用/启用账号;初始超级管理员由配置在启动时幂等创建;禁用/删除账号的会话立即失效。 - **仪表盘统计**:用户总数/今日新增/各状态分布、订单总数/各状态分布、累计与今日营收、累计退款、待处理退款数、公开展示中的穿搭数、已封禁穿搭数、上架商品数,一次接口聚合返回。 - **用户管理**:分页搜索(昵称/手机号/OpenID/状态)、详情(含永久额度 + 每日额度 + 额度凭证列表)、封禁/解封(临时按天/永久,写原因与期限)、额度发放(有有效期额度凭证,同事务写人工调整流水)、额度凭证回收(同事务写人工调整流水)、VIP 人工调整(事务 + 人工调整流水)。 - **商品管理**:分页列表(含下架)、新增、编辑(含 `valid_days` 额度有效期天数)、上下架、软删除;支持登记/修改微信虚拟支付道具 ID 与发布状态(仅「未发布/已发布」两态,已发布必须填道具 ID,道具 ID 全局唯一),运营无需再手工改库。**运营约定**:道具在微信后台手动创建;改价后必须同步微信侧道具价格,否则用户付款后发货金额校验会拒绝(拿不到权益)。 - **订单与退款管理**:订单列表/详情(含商品类型与额度有效期天数快照、虚拟支付 pay_channel=2 与 XPay 平台/环境/道具字段);退款申请审核——通过置「退款中」(实际打款由运营在微信商户平台手动完成)、驳回置「失败」并记录原因。 - **内容审核**:公开穿搭列表/详情(含衣物明细)、封禁/解封(**主要治理手段**)、审核通过/驳回(保留但已无待审数据,见「关键业务规则」);衣物列表/详情;衣柜分享列表、封禁/解封。 - **通知推送**:单用户系统通知、全量广播(分批写入 `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/3/0;未配置则回落内置默认 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 过期(业务码 `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` | 核心指标聚合(`todo` 为:`refund_pending` 待处理退款、`outfit_public_total` 公开展示中的穿搭数、`outfit_banned` 已封禁穿搭数、`goods_on_shelf` 上架商品数;`content.outfit` 为公开/私密/已封禁分布) | **用户管理**(路径参数 `: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);同事务写 `quota_flow_logs` 人工调整流水 | | PUT | `/users/:uuid/vip` | VIP 人工调整(json:vip_type、vip_days、reason) | | DELETE | `/users/:uuid/grants/:id` | 回收额度凭证(软删 + 归属校验);同事务写 `quota_flow_logs` 人工调整流水 | **商品管理** | 方法 | 路径 | 说明 | | --- | --- | --- | | 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/transaction_id/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)。**兼容保留**:业务侧公开即自动过审,实际已无 `audit_status=1` 的数据可操作,仅允许操作待审记录,否则返回状态不允许 | | 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 广告激励 / 4 体验会员赠送)。管理后台「额度发放」写入 `quota_grants`(source=2 加量包、`ref_id=manual_`、`expire_time=now+expire_days*86400`):Cos 为一次性库存,AI/社区/衣柜 为有效期内每日加量;「凭证回收」软删除对应凭证,仅「未用完的部分」失效(`Amount-Used`)。有效额度 = 免费配置值(`Quota` 段,须与业务服务 `UserDefaultQuota` 一致)+ 有效凭证,每日已用读 `user_daily_usages`。 - **额度流水闭环**:管理后台的「额度发放」「凭证回收」在**同一事务内**写 `quota_flow_logs`(`change_type=5` 人工调整、`change_target=1`),流水的 `old_value`/`new_value` 取事务内真实剩余额度,保证「变更前 + 变更量 = 变更后」;发放流水的 `order_no` 用凭证 `ref_id`(与业务侧到期失效流水按 `ref_id` 关联的口径一致),回收流水使用独立的 `revoke_`,避免撞 `uk_quota_flow_idem(order_no, quota_type, change_type)` 唯一索引。 - **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`(**公开即自动过审**),因此本管理后台的「审核通过/驳回」已无待审数据(接口保留仅为兼容),仪表盘也不再输出「待审核穿搭」,改为输出「公开穿搭总数(不含已封禁)」与「已封禁穿搭数」。对外可见条件为 `is_public=2` 且 `audit_status=2` 且 `status=1`,**内容治理以封禁(`status=2`)为主**:封禁后立即从小程序社区、衣物社区入口与穿搭分享中隐藏,且用户自行「取消公开 → 再次公开」也无法恢复可见(公开动作只改 `is_public`/`audit_status`,不改 `status`)。 - **通知未读计数**:管理后台自增业务服务的同一 Redis key(`ztcloset:notification:unread:`),要求两者共用同一 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` 即可零开销关闭。