Files
full/docs/market.md

254 lines
38 KiB
Markdown
Raw Normal View History

2026-09-22 18:53:53 +08:00
# market代理商与供应商管理代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/ec/market` |
| 服务域 | 电商域EC——渠道侧代理商 / 供应商)与渠道经营数据 |
| 审计日期 | 2026-09-22 |
| 代码规模 | Go 文件 49 个:`pb/` 生成代码 10 个 7125 行;非 `pb/` 39 个 1569 行(其中 protoc-gen-slc 生成骨架 4 个 185 行、测试 1 个 13 行、手写类型别名 1 个 3 行)→ 业务手写约 1368 行。`proto/` 4 个 510 行;`etc/` 4 个 123 行;`sdk/typescript/` 5 个 2465 行;`test/*.http` 10 个 81 行;`README.md` 3 行 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、`cmd/cli`(脚手架占位)、聚合入口 `pkgs/all``service.Expose` 复用共享 gRPC/Gateway |
| 对外协议 | gRPC + HTTP Gateway路径 `/market.<Agency|Supply|Data>/<Method>`,全部 POST |
| 结论摘要 | **审核流程与账号生命周期完全未被保护**`Approve` 全函数没有任何鉴权调用,`Agency.Create`/`Agency.Delete`/`Supply.Create` 的鉴权被整段注释;`Get`/`Modify`/`SetPassword` 用请求里的 `identity` 覆盖 token 里的 `identity`,可读改任意代理商资料;登录签发的 AES 密文与所有受保护接口使用的 JWT 校验**互不兼容**(合法登录后无法调用任何接口,且可触发 SDK 空指针)、`Data` 服务 5 个方法全部是返回 `nil` 的占位、分页存在 `Offset` 早于归一化与 `LIMIT 0` 两处确定性缺陷、列表返回的 `count` 是本页条数。密码已统一 bcrypt无 MD5 代码),但 `salt` 列残留且无历史哈希兼容分支。 |
## 1. 服务定位与职责
渠道(代理商 / 供应商)主数据与渠道经营数据的对外服务,由三个 gRPC service 组成(`internal/server/new.go:33-35`
- `Agency``proto/agency.proto:7-35`):代理商(省代公司)登录、建号、增删改查、改密、待审核列表、审核;
- `Supply``proto/supply.proto:7-24`):供应商建号、增删改查;
- `Data``proto/data.proto:7-22`):代理商视角的概况/会员/订单统计(**当前全部为占位**)。
它是渠道账号与资质的**管理中心**:账号体系是自建的一套(`market_agency` / `market_supply` 两张表各自存 `account`/`password`,与 base/passport 的会员体系无关);审核(`approve` 0 待审 / 2 通过 / -2 拒绝)是登录前置条件(`login.go:44`)。它不负责佣金结算、不负责订单归属计算,也没有与订单/会员表的联动(`data` 服务为空实现,见 6.3)。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New``impl.NewImpl``server.New(nil)``service.New``srv.Start` |
| `cmd/cli/main.go` | 脚手架占位,仅 `log.Println("Hello World!")``cmd/cli/main.go:5-7` |
| `internal/config/config.go` | 配置结构Base、Databases、MicroService、Rpc、Gateway、Apm、Etcd与校验 |
| `internal/impl/impl.go` / `with.go` | Redis / DB / Etcd 初始化(`RedisCache`/`Etcd`/`DBService` |
| `internal/models/market_agency.go` | `MarketAgency` 模型(代理商) |
| `internal/models/market_supply.go` | `MarketSupply` 模型(供应商) |
| `internal/models/impl.go` | GORM 连接与 `AutoMigrate`(两张表) |
| `internal/models/query.go` | `NormalStatus`/`DisabledStatus` 常量 + 写入 token 的 `Std_Market` 结构 |
| `internal/password/password.go` | bcrypt `Hash`/`Verify`12 行,唯一有单元测试的包) |
| `internal/server/{new,agency_server,supply_server,data_server}.go` | 生成骨架:注册 3 个 gRPC service、按 service 转发到 logic |
| `internal/logic/agency/*.go`10 个) | 登录/建号/查/列表/改/删/改密/待审/审核/字段映射 |
| `internal/logic/supply/*.go`6 个) | 供应商建号/查/列表/改/删/字段映射 |
| `internal/logic/data/*.go`5 个) | 概况/会员列表/会员详情/订单列表/订单详情(**全部占位** |
| `service/expose.go` / `dependencies.go` | 聚合宿主注入(`applyDependencies` + 3 个 Handler 注册) |
| `proto/*.proto` | 4 个契约文件(`const.proto` 为跨模块复制的 blocks 定义) |
| `test/agency/*.http``test/supply/fetch.http` | 10 个手工请求脚本(非自动化测试) |
依赖注入:`service.Dependencies``service/dependencies.go:12-17`)非 nil 时覆盖 `internal/impl` 包级变量,聚合实例见 `pkgs/all/internal/service/market.go:10-19`;同样地,`Dependencies.Cache` 被声明但 `applyDependencies``:19-29`)忽略。
## 3. 接口清单
全部接口注册在 grpc-gateway 的 POST 路由上Agency`pb/agency.pb.gw.go:287,307,327,347,367,387,407,427,447`Supply`pb/supply.pb.gw.go:408-412`Data`pb/data.pb.gw.go:410` 等):
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/market.Agency/Login` | 代理商登录,签发 token | **无**(仅校验入参非空) | `internal/logic/agency/login.go:19` |
| POST | `/market.Agency/Create` | 新增代理商 | **无(鉴权被注释掉)** | `internal/logic/agency/create.go:19``:20-23` 注释) |
| POST | `/market.Agency/Get` | 取代理商 | token + **identity 可被请求覆盖** | `internal/logic/agency/get.go:16``:25-27` |
| POST | `/market.Agency/Fetch` | 代理商列表 | token不校验角色/归属) | `internal/logic/agency/fetch.go:16` |
| POST | `/market.Agency/Modify` | 改代理商 | token + **identity 可被请求覆盖** | `internal/logic/agency/modify.go:16``:25-27` |
| POST | `/market.Agency/Delete` | 删代理商 | **无(鉴权被注释掉)** | `internal/logic/agency/delete.go:16``:17-21` 注释) |
| POST | `/market.Agency/SetPassword` | 改密码 | token + identity 可覆盖 + 需旧密码 | `internal/logic/agency/set_password.go:19``:25-27,37` |
| POST | `/market.Agency/Pending` | 待审核列表 | token不校验角色 | `internal/logic/agency/pending.go:16` |
| POST | `/market.Agency/Approve` | 审核(通过/拒绝) | **无(全函数无任何鉴权调用)** | `internal/logic/agency/approve.go:15` |
| POST | `/market.Supply/Create` | 新增供应商 | **无(鉴权被注释掉)** | `internal/logic/supply/create.go:18``:19-23` 注释) |
| POST | `/market.Supply/Get` | 取供应商 | token按请求 identity 查) | `internal/logic/supply/get.go:15` |
| POST | `/market.Supply/Fetch` | 供应商列表 | token不校验角色 | `internal/logic/supply/fetch.go:16` |
| POST | `/market.Supply/Modify` | 改供应商 | token按请求 identity 改) | `internal/logic/supply/modify.go:17` |
| POST | `/market.Supply/Delete` | 删供应商 | token按请求 id/identity 删) | `internal/logic/supply/delete.go:17` |
| POST | `/market.Data/Overview` | 概况统计 | token**实现为空** | `internal/logic/data/overview.go:11` |
| POST | `/market.Data/MemberFetch` | 会员列表 | token**实现为空** | `internal/logic/data/member_fetch.go:11` |
| POST | `/market.Data/MemberDetails` | 会员详情 | token**实现为空** | `internal/logic/data/member_details.go:12` |
| POST | `/market.Data/OrderFetch` | 订单列表 | token**实现为空** | `internal/logic/data/order_fetch.go:11` |
| POST | `/market.Data/OrderDetails` | 订单详情 | token**实现为空** | `internal/logic/data/order_details.go:12` |
补充说明:
- **声明但未实现/占位(已核实)**`Data` 服务的 5 个方法**全部是骨架**——`overview.go:18-22``member_fetch.go:26-28``member_details.go:24-26``order_fetch.go:26-28``order_details.go:24-26` 都是"校验 token+ 归一化分页参数)→ `// TODO: add your logic code & delete this line.``return`",命名返回值 `reply` 从未赋值,实际返回 `(nil, nil)`。其中 `overview.go:18` 还额外留着 `// TODO: valid code`。**不是"疑似",是确认的空实现**`proto/data.proto:24-39` 里的 `OverviewReply`4 个计数 + 2 组按月报表)与 `DataReply`/`KeyVal` 永远不会被填充。
- 上表"无"的鉴权,指的是 `internal/logic` 层没有任何校验(`ParseMetaCtx` 被注释或根本不存在)。聚合入口 `pkgs/all` 额外安装了 gRPC 拦截器与 HTTP 中间件(`pkgs/all/internal/server/authorization.go:42-53,55-66`),按 `Authorization.Anonymous` 白名单放行、其余要求一个合法 JWT —— 因此聚合部署下"无鉴权"退化为"**任意合法 JWT 均可**"(不校验角色、不校验归属);独立进程部署(本模块 `cmd/main``internal/server/new.go:23``grpc.NewServer()` 无拦截器)下则是彻底匿名可用。`Authorization.Anonymous` 白名单文件不在仓库内(`pkgs/all/etc/` 不存在)→【信息不足】无法确认聚合部署下哪些方法被列为匿名。
## 4. 数据模型与表
### 表 `market_agency``internal/models/market_agency.go:11-37`GORM 自动迁移 `internal/models/impl.go:15-18,39`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 自增主键 |
| `identity` | varchar(36) | uniqueIndex | 唯一标识,写入 `utils.ULID()``create.go:32` |
| `created_at` / `updated_at` | TIMESTAMP | - | 时间戳 |
| `deleted_at` | TIMESTAMP | index | GORM 软删除列 |
| `status` | int8 | default 0, index | -1 禁用(`models/query.go:7`/ 1 正常(`create.go:32` 硬编码) |
| `name` | varchar(255) | **not null** | 联系人名称 |
| `avatar` | varchar(255) | default '' | 头像 |
| `account` | varchar(255) | default ''**无唯一索引** | 登录账号(登录按此字段查,`login.go:26` |
| `phone` | varchar(20) | **not null** | 手机号(无格式约束) |
| `password` | varchar(255) | **无 not null** | bcrypt 哈希(`create.go:27` |
| `salt` | varchar(255) | default '' | 历史盐字段:无读取点,`set_password.go:47` 还在显式写 `""` |
| `email` | varchar(255) | default '' | 邮箱 |
| `country` / `area` | varchar(255) | default '' | 国家 / 地区 |
| `org_name` / `org_photo` | varchar(255) | default '' | 机构名称 / 照片 |
| `id_name` / `id_before` / `id_after` | varchar(255) | default '' | 证件姓名 / 正面照 / 反面照 |
| `remark` | varchar(255) | default '' | 备注 |
| `commission_rate` | int32 | default 0 | 佣金比例(**无范围约束**,可写负数或超大值) |
| `agency_type` | int32 | default 0 | 1 代理商 / 2 安装商(无校验) |
| `approve` | int8 | default 0 | 0 待审 / 2 通过 / -2 拒绝(`login.go:44` 强制 `==2` 才能登录) |
| `last_login_at` | TIMESTAMP | - | 无 `gorm` tag按 GORM 命名推断为 `last_login_at`;登录时用 `Select("last_login_at")` 单独更新(`login.go:60` |
### 表 `market_supply``internal/models/market_supply.go:10-32`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` / `created_at` / `updated_at` / `deleted_at` / `status` | 同上 | 同 `market_agency` | 结构一致 |
| `name` | varchar(255) | not null | 联系人名称 |
| `avatar` / `account` | varchar | default '' | 头像 / 账号(`Create``Avatar` 被注释掉,见 `supply/create.go:34`,永远写不进去) |
| `phone` | varchar(20) | not null | 手机号 |
| `password` | varchar(255) | **not null** | bcrypt 哈希 |
| `salt` | varchar(255) | default '' | 同上,无使用点 |
| `org_name` / `org_photo` / `id_name` / `id_before` / `id_after` / `remark` | varchar(255) | default '' | 机构与证件信息 |
| `commission_rate` | int32 | default 0 | 佣金比例(无范围约束) |
| `approve` | int8 | default 0 | 0 / 1 审核中 / 2 / -2注释比代理商表多一档"1 为审核中",但**没有任何代码写 1** |
两张表的共同缺口:`account` 无唯一索引(可重复注册同名账号,见 6.2`status``approve``commission_rate``agency_type` 均无 check 约束;`password` 长度未按 bcrypt 输出60 字符)收紧到 60。
## 5. 核心流程
```mermaid
flowchart TD
A["POST /market.Agency/Login"] --> B["按 account 查 market_agency 取第一条"]
B --> C["password.Verify 走 bcrypt"]
C --> D["校验 status 不等于 -1 且 approve 等于 2"]
D --> E["encipher.GenerateTokenAes 签发 AES-CBC 密文"]
E --> F["Select last_login_at 更新并返回 token 与账号信息"]
G["后续任意受保护接口"] --> H["ParseMetaCtx 转 token.ParseJwt 期望 HS256 JWT"]
H --> I["AES 密文非三段 JWT校验必然失败且 SDK 在解析失败后访问 nil token 会 panic"]
J["POST /market.Agency/Create"] --> K["鉴权被注释bcrypt 建号 Status=1 approve=0"]
L["POST /market.Agency/Approve"] --> M["全函数无鉴权,按 identity 直接改 approve=2 或 -2"]
K --> N["approve 必须为 2 才允许登录"]
M --> N
O["POST /market.Agency/Get 或 Modify 或 SetPassword"] --> P["identity 用请求值覆盖 token 值"]
P --> Q["按覆盖后的 identity 读或改任意代理商记录"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/agency/approve.go:15-46` | **审核接口完全无鉴权**。整个函数没有任何 `service.ParseMetaCtx` 调用(对比同目录 `get.go:20``fetch.go:22``modify.go:20` 都调用了;`Login` 不调用是合理的,因为它就是登录入口),只校验 `identity` 非空(`:17-19`)。任何人只要能把请求送到(独立部署时直连 gRPC 端口即可),就能把任意代理商 `approve` 改成 2通过`:29`)或 -2拒绝`:34`)。审核是登录的唯一前置条件(`login.go:44`),此接口失守等于渠道准入体系失守。 |
| **高** | `internal/logic/agency/create.go:20-23``internal/logic/supply/create.go:19-23` | **建号鉴权被整段注释**:两处的 `// parse authorization meta.` 后跟 `// _, err = service.ParseMetaCtx(ctx, nil)` 及其错误分支都被注释掉,函数直接进入参数校验。配合 `create.go:32` 硬编码 `Status: 1`,任何人都能创建可用的代理商/供应商账号;再配合上一条,任何人都能自己把自己审核通过。 |
| **高** | `internal/logic/agency/delete.go:17-21,26` | **删除接口鉴权同样被注释**,且删除条件为 `Where("id = ? or identity = ?", in.GetId(), in.GetIdentity())``:26`),两个条件任一命中即删。匿名调用即可软删除任意代理商(`deleted_at` 置位后该账号无法登录,等同封号),也支持传 `id` 批量打击。 |
| **高** | `internal/logic/agency/get.go:25-27``modify.go:25-27``set_password.go:25-27` | **请求参数覆盖 token 身份**:三处都是 `identity = auth.Identity; if in.GetIdentity() != "" { identity = in.GetIdentity() }`,随后的查询/更新(`get.go:29``modify.go:45``set_password.go:33`)全部使用被覆盖后的 `identity`。任意已登录账号传一个他人的 `identity` 即可:`Get` 读出对方 `id_name`/证件照/`phone`/`email``get.go:33-52` 全字段返回)、`Modify` 改掉对方 `account`(登录名)、`phone``email``commission_rate``org_name``modify.go:28-45`)。`SetPassword` 因需旧密码(`:37`)危害较小,但仍可对任意目标发起撞库式改密。 |
| **高** | `internal/logic/agency/fetch.go:16-52``pending.go:16-47` | **列表接口只验"token 有效"、不验角色、不按归属过滤**`fetch.go:22``pending.go:17` 都是 `service.ParseMetaCtx(ctx, nil)`,无任何 `Where` 限定调用方。任何持合法 JWT 的调用方(含商城会员等其他业务域角色;独立进程部署下连 JWT 都不需要)都能一次拉走**全部代理商**(含待审核)的 `phone`/`email`/`id_name`/证件照 URL`fetch.go:30-32` 只把 `page_size < 10` 归一到 50、**没有上限**,传 `page_size=1000000` 即全量导出。 |
| **高** | `internal/logic/agency/login.go:53` + SDK `crypto/token/jwt.go:64-73``golang-jwt/jwt/v5@v5.3.1/parser.go:138-140` | **登录签发的令牌无法被任何接口接受,且会触发空指针 panic**`Login``encipher.GenerateTokenAes` 生成的是 **AES-CBC 密文的 base64**SDK `crypto/encipher/encipher.go:29-54`),而 `service.ParseMetaCtx` 走的是 **HS256 JWT 解析**SDK `service/meta.go:31``token.ParseJwt`。base64 密文里没有点号,`jwt.ParseWithClaims` 在第一段就返回 `nil, ErrTokenMalformed`parser.go:139-140而 SDK 随后执行 `if claims, ok := token.Claims.(*Claims); ...`jwt.go:68**在 nil 上取字段 → 空指针 panic**;模块 `internal/server/new.go:23``grpc.NewServer()` 未装 recover 拦截器goroutine panic 会终止进程。两个后果:① 合法登录的代理商调用任何受保护接口都失败(聚合入口的 `authorization.validate` 同样只认 JWT`pkgs/all/internal/server/authorization.go:73-85`);② 任何人向 gRPC 端口发一个 `Authorization: x` 就能让进程崩溃。 |
| 中 | `internal/logic/agency/create.go:50,52` | `fmt.Println("mktModel = ", mktModel)` 把**包含 bcrypt 密码哈希的完整模型**打印到 stdout`:52` 还打印 DB 错误)。生产由 supervisor 托管stdout 会落到 `stdout_logfile``etc/supervisor.bsm-ec-market.conf:8`),等于把口令哈希写进日志文件。 |
| 中 | `internal/logic/agency/login.go:22-46` | 登录接口无失败次数限制、无锁定、无验证码校验:`proto/agency.proto:41` 声明的 `verify` 字段从未被读取(`:22` 只判 `Account`/`Password` 非空),且 `ErrAccountNotFound``:29`)与 `ErrPassword``:35`)分开返回,可用于账号枚举 + 无限撞库。 |
| 中 | `internal/logic/agency/login.go:60` | 业务查询上残留 `impl.DBService.Debug()`,会为该条 UPDATE 打开 GORM 调试输出(含参数),与 `internal/models/impl.go:83` 的全局 Debug 叠加。 |
| 中 | `internal/logic/agency/get.go:29-32``modify.go:46-47``supply/get.go:23-26``supply/modify.go:39-40` | 查不到记录时直接 `return nil, err` 透传 **GORM 原始错误**(而非 `errcode`),把表名/驱动错误信息经 API 返回外部;`log.Println("获取代理商数据失败:", err)` 的中文日志也与 SDK `printer` 风格不一致。 |
| 低 | `internal/password/password.go:5-12` | **密码哈希已无 MD5 遗留(代码层)**`Hash`/`Verify` 统一 `bcrypt.GenerateFromPassword`DefaultCost调用点只有 `create.go:27``supply/create.go:27``set_password.go:41``login.go:34`,全仓库无 md5 引用;`internal/password/password_test.go:5-13` 覆盖了正确/错误口令两个分支。遗留形态是**数据侧**`market_agency.salt``models/market_agency.go:18`)与 `market_supply.salt``models/market_supply.go:17`)两列仍存在,`set_password.go:47` 还在显式写 `"salt": ""`,且登录/改密**没有任何"历史 MD5 或加盐哈希"的兼容或迁移分支**——若库中存量值是旧哈希,`bcrypt.CompareHashAndPassword` 必然返回 false用户侧无自助修复路径。【信息不足】本次未连接数据库无法确认线上两张表的 `password` 列是否仍有 32 位 MD5或加盐 MD5存量值。 |
| 低 | `etc/market_dev.yaml:8` | dev 配置写了 `Databases.Debug: true`,但 `conf.DBConf` 只有 `Driver`/`Source`SDK `conf/types.go:18-21`),该键被 YAML 解析器静默忽略;真正生效的是 `internal/models/impl.go:83` 的无条件 `db.Debug()`。配置看起来可控、实际不可控。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/agency/fetch.go:19,27-32,43` | **`Offset` 在分页参数归一化之前就算好了**。`:19` 用请求原始值计算 `Offset = (page_no-1)*page_size``:27-32` 才把 `page_no<1` 归一为 1、`page_size<10` 归一为 50`:43` 用**归一后的 `Limit` + 归一前的 `Offset`**。两个必现后果:① 只传 `page_no``page_size` 缺省 0`Offset` 恒为 0 → 无论要第几页都返回第一页数据;② 传 `page_no=3,page_size=5``Offset=10``LIMIT 50` → 返回的是"第 11 条起的 50 条",页窗口整体错位(第 2、3 页内容重叠)。`supply/fetch.go:19,27-32,37` 是同一份代码的复制,缺陷相同。 |
| **高** | `internal/logic/agency/pending.go:31,38` | **不传 `page_size` 时待审核列表恒为空**。归一化用的是 `if in.GetPageSize() < 0 { in.PageSize = 50 }``:31`),而 `page_size` 缺省值是 0不满足 `< 0`)→ 0 被原样传给 `Limit(0)``:38`GORM 对 `Limit(0)` 生成 `LIMIT 0``gorm.io/gorm/clause/limit.go:16-18` 的条件是 `*Limit >= 0`),查询结果必为空。对照 `fetch.go:30` 用的是 `< 10`,两处阈值不一致。`test/agency/pending.http:5` 恰好传了 `page_size`,所以手工验证时看不出问题。 |
| **高** | `internal/logic/agency/fetch.go:43,50``pending.go:38,45``supply/fetch.go:37,43` | **`count` 返回的是本页条数而非总数**。三处都执行了 `Count(&cnt)` 却把 `cnt` 丢弃,返回体写的是 `Count: int64(len(data))`=这一页的行数)。前端据此算总页数必然错误;`cnt` 变量在三处都是"算了不用"的死赋值。 |
| 中 | `internal/logic/agency/approve.go:27-38,40-44` | `switch in.GetApprove()` 只处理 1`approve=2``:29`)和 2`approve=-2``:34`**其它值(含缺省 0、或任意数字什么都不做却返回 `Code:0, Message:"OK"`**,调用方会误判审核成功。函数还不校验当前状态(可对已通过/-2 的记录反复改判)、不记录审核人与审核时间(无审计字段),返回体也没有 `Timeseq`(对比 `delete.go:35`)。 |
| 中 | `internal/logic/agency/ref.go:16-35` | 列表字段映射**漏掉 `Approve`**`proto/agency.proto:65` 定义了该字段):`/market.Agency/Fetch``/Pending` 返回的 `approve` 恒为空,前端拿不到审核状态,`Pending` 的结果也无法体现"待审 vs 已拒绝"。`supply/ref.go:14-29` 同样漏了 `Approve``CommissionRate``supply/get.go:28-41` 也漏了这两个字段)。 |
| 中 | `internal/logic/agency/modify.go:28-45,53` | `Updates(&mktModel)` 传结构体 → GORM 跳过零值:`commission_rate` 改不回 0、`remark`/`email` 等无法清空;`Account`(登录名)可被任意改;`in.Password``proto/agency.proto:52`)被静默忽略。同时 `:53``Identity: mktModel.Identity` 取的是**刚构造的入参结构体**`:28-44` 从未赋值该字段)→ 响应里的 `identity` **恒为空字符串**,调用方拿不到被改对象标识。`supply/modify.go:38-46` 同一问题(返回的 `Identity` 也是空)。 |
| 中 | `internal/logic/agency/create.go:24-49``supply/create.go:24-44` | **不校验 `account` 唯一性**,而 `account` 列没有唯一索引(`models/market_agency.go:15``models/market_supply.go:14`)→ 可创建任意多个同 `account` 账号;登录用 `Where("account=?").First(...)``login.go:26`)取无序的第一条,登录到哪个账号不确定(可能恰是刚被审核拒绝的那条,表现为"密码错误/账号被禁用")。`create.go:25` 只校验 name/account/password 非空,`phone`(列为 not null 但允许空串)等未校验。 |
| 中 | `internal/logic/agency/create.go:31-49` | 创建时忽略请求里的 `status``approve`,硬编码 `Status: 1``approve` 取零值 0。`approve=0` 符合"待审核"语义,但 `proto/agency.proto:61` 声明的 `status` 被静默丢弃,接口契约与行为不符。 |
| 中 | `internal/logic/agency/fetch.go:35``pending.go:36` | `tx.Where(...)` 未回写到 `tx`,而同函数的 `:38/:41`Fetch`:34`Pending都写了同一函数内两种写法并存。当前依赖 GORM "clone=0 时共享 Statement" 的内部行为仍能生效,属隐患而非现网故障,但版本升级或链式顺序调整后会静默丢条件。 |
| 低 | `internal/logic/supply/get.go:23` | 若请求 `identity` 为空则生成 `Where("identity=''")`,直接返回 GORM `ErrRecordNotFound` 原始错误;`supply.Modify` 有非空校验(`modify.go:22-24``supply.Get` 没有,同目录口径不一致。 |
| 低 | `internal/models/query.go:4-7` | `NormalStatus` 常量无任何引用(只有 `DisabledStatus``login.go:39` 使用)。 |
### 6.3 未完成实现
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/data/overview.go:18-22` | `Overview`(概况)保留 `// TODO: valid code``// TODO: add your logic code & delete this line.`,直接 `return` 返回 `(nil, nil)``proto/data.proto:24-39``total_approve`/`total_month`/`total_all`/`total_staff``report_month`/`report_staff` 永远为空。 |
| **高** | `internal/logic/data/member_fetch.go:26-28``member_details.go:24-26``order_fetch.go:26-28``order_details.go:24-26` | 会员列表、会员详情、订单列表、订单详情 4 个接口同样是"只做鉴权(+分页参数归一)+ TODO + `return`"的骨架,`reply` 从未赋值。**`market.Data` 服务的 5 个方法零实现**,接口已对外注册(`internal/server/data_server.go:19-41``service/expose.go:26-28`),对调用方表现为"永远返回空"。 |
| 中 | `internal/logic/data/member_fetch.go:19-24``order_fetch.go:19-24` | 占位实现里归一化出的 `PageNo`/`PageSize` 无任何消费者(没有查询),且用 `< 10 → 50``pending.go:31``< 0 → 50`,同一模块三种分页口径;占位接口同样没有 `page_size` 上限。 |
| 中 | `proto/supply.proto:29-46` + `internal/logic/supply/create.go:27,35-36` | 供应商表有 `account`/`password`/`last_login_at` 字段、`Create` 也在写 bcrypt 哈希,但**整个 `Supply` 服务没有任何登录接口**`proto/supply.proto:7-24` 只有 Create/Get/Fetch/Modify/Delete`supply/create.go:34` 还把 `Avatar` 的赋值注释掉了(字段永远为空)。供应商账号目前只能建、不能用。 |
| 低 | `internal/logic/agency/create.go:19-23``delete.go:16-21``supply/create.go:17-23` | 三处保留被注释掉的 `// parse authorization meta.` 段落,属"注释后忘记打开"的遗留,不是有意匿名设计(同目录其它文件都调用了 `ParseMetaCtx`)。 |
| 低 | `cmd/cli/main.go:5-7` | 仍是 `log.Println("Hello World!")` 脚手架。 |
| 低 | `README.md:1-3` | 只有 `# market` 和一行 `market agency`,无启动方式、账号体系与审核流程说明。 |
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/models/impl.go:75-85`(尤其 `:83` | **生产环境强制打开 SQL 调试日志**`NewPostgres` 无条件 `db = db.Debug()`,三个 yaml 的 `Driver` 都是 `postgres``etc/market_dev.yaml:5``market_prod.yaml:5``market_test.yaml:5`)→ 全部 SQL 与参数打印,包含代理商 `password` 哈希、`id_name`、证件照 URL、`phone``email`。 |
| 中 | `internal/models/impl.go:39` | 每次启动执行 `AutoMigrate(&MarketAgency{}, &MarketSupply{})`(生产同样执行);模型字段变更会直接改线上表结构,无版本化、无回滚、发布前无法评审 DDL。 |
| 中 | `internal/server/new.go:16,26-30` + `cmd/main/main.go:32` | **独立进程下 HTTP 网关不可用**`Server.Mux` 字段声明后**从未赋值**(全仓库检索 `.Mux =` / `Mux:` 无命中),`cmd/main` 把它nil`GatewayMux` 传给 SDKSDK `service.Start` 用它执行 `http.ListenAndServe(httpAddr, s.Opts.GatewayMux)`SDK `service/service.go:106-108`)→ 非空接口持有 nil 指针,`(*ServeMux).ServeHTTP` 首次访问 `s.unescapingMode`grpc-gateway `runtime/mux.go:407-420`)即空指针 panic`net/http` recover 后断开连接。只有聚合入口 `pkgs/all` 自建 ServeMux`pkgs/all/internal/server/server.go:38`)才正常。同一缺陷存在于全部模块的生成骨架,属生成器级问题。 |
| 低 | `internal/impl/impl.go:9``internal/impl/with.go:19-21` | `RedisCache``Etcd` 初始化后无任何使用点(死代码);`service/dependencies.go:16``Cache` 字段被 `applyDependencies``:19-29`)忽略。 |
| 低 | 全模块 | 只有 `internal/password/password_test.go:5-13` 一个单元测试;`internal/logic/**` 无测试、`internal/models` 无测试。本次发现的分页错位、`LIMIT 0``count` 语义、审核空转switch 无 default 分支)等问题都无回归保护。 |
| 低 | `internal/logic/agency/{login.go:53,create.go:50}` | 使用标准库 `log`/`fmt.Println`/`fmt.Errorf` 与 SDK `printer` 混用(`fetch.go:44``delete.go:27``printer.Error`),日志出口与格式不统一;`fmt.Println` 的调试输出被直接留在生产路径上。 |
| 低 | `etc/market_dev.yaml:20``etc/market_prod.yaml:19``etc/market_test.yaml:19` | `MicroService.Anonymous` 三个环境不一致dev 是 `market.ping.hello`prod/test 是 `mall.ping.hello`),且本模块**没有任何 ping 方法**`proto/*.proto` 只有 Agency/Supply/Data 三个服务)→ 该白名单条目恒为无效配置;`SecretKey: CHANGE_ME``password=CHANGE_ME` 也是占位值。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| M1 | 高 | `Approve` 无任何鉴权;`Agency.Create`/`Agency.Delete`/`Supply.Create` 鉴权被注释(`approve.go:15-46``create.go:20-23``delete.go:17-21``supply/create.go:19-23` | 渠道准入体系可被绕过、账号可被自助创建与删除 |
| M2 | 高 | `Get`/`Modify`/`SetPassword` 用请求 `identity` 覆盖 token `identity``get.go:25-27``modify.go:25-27``set_password.go:25-27` | 任意代理商 PII证件照/手机号/邮箱)可读、账号资料可改 |
| M3 | 高 | `Data` 服务 5 个方法全为零实现却已注册对外(`logic/data/*.go:18-28` | 功能不可用、API 契约撒谎 |
| M4 | 高 | 登录签发 AES 密文 vs 接口校验 HS256 JWT 互斥(`login.go:53`、SDK `crypto/token/jwt.go:64-73` | 登录后无法调用任何受保护接口 |
| M5 | 高 | 非 JWT 的 `Authorization` 头触发 SDK `ParseJwt` 空指针,模块无 recover 拦截器(`internal/server/new.go:23` | 服务可用性(进程崩溃,任意人可触发) |
| M6 | 高 | 分页 `Offset` 早于参数归一化;`Pending``< 0` 归一导致 `LIMIT 0``fetch.go:19,27-32,43``pending.go:31,38``supply/fetch.go:19,27-32,37` | 列表接口返回错页或恒空 |
| M7 | 高 | `Count` 被丢弃、返回本页条数(`fetch.go:43,50``pending.go:38,45``supply/fetch.go:37,43` | 前端分页计算错误 |
| M8 | 中 | 列表接口仅验 token、无角色/归属过滤、`page_size` 无上限(`fetch.go:22,30-32``pending.go:17` | 批量 PII 泄露(含证件照) |
| M9 | 中 | 登录无锁定、账号枚举、`verify` 未校验(`login.go:22-46` | 撞库、账号枚举 |
| M10 | 中 | 建号日志打印含密码哈希的完整模型(`create.go:50,52` | 凭据泄漏至日志文件 |
| M11 | 中 | 审核空转返回成功、不校验当前状态、无审核留痕(`approve.go:27-44` | 审核结果不可信、无法追责 |
| M12 | 中 | `account` 无唯一索引且不查重(`models/market_agency.go:15``create.go:24-49``login.go:26` | 登录串号、账号抢占 |
| M13 | 中 | 列表映射漏 `Approve``Modify` 返回空 `identity``ref.go:16-35``modify.go:53``supply/modify.go:46` | 前端拿不到审核状态与变更对象标识 |
| M14 | 中 | 生产强制 SQL Debug + 启动 `AutoMigrate``internal/models/impl.go:39,83` | 敏感信息落盘、DDL 不可控 |
| M15 | 中 | 独立进程 HTTP 网关因 `Mux` 未赋值不可用(`internal/server/new.go:16``cmd/main/main.go:32` | 可用性(生成器级、全模块共有) |
| M16 | 中 | 供应商有密码字段却无登录接口、`Avatar` 赋值被注释(`proto/supply.proto:7-24``supply/create.go:34` | 功能不可用 |
| M17 | 低 | `salt` 列残留 + 无历史哈希兼容分支;死代码、无测试、`cmd/cli`/README 占位 | 存量账号登录(【信息不足】)、可维护性 |
## 8. 修复建议(务实项)
1. **补齐四处鉴权M1最高优先**`approve.go` 开头补 `auth, err := service.ParseMetaCtx(ctx, nil)`(它还需要一个"审核人"概念,可先用返回值做非空校验);`create.go:20-23``delete.go:17-21``supply/create.go:19-23` 把注释掉的段落恢复。这样至少保证"必须持合法令牌",与同目录其它接口一致。
2. **禁止 identity 覆盖M2**`get.go:25-27``modify.go:25-27``set_password.go:25-27` 统一改为只使用 `auth.Identity`;若确实需要"管理员代改",就显式判定 `auth` 的某个角色扩展字段(`claims.Extend`SDK `service/meta.go:51-60`),未带该角色时直接 `errcode.ErrPermissionDenied`
3. **列表接口收口M8/M13**`fetch.go:22``pending.go:17``ParseMetaCtx(ctx, nil)` 改为传入角色要求;`ref.go:16-35` 补上 `Approve``supply/ref.go``Approve`/`CommissionRate``fetch.go:30-32` 增加上界(例如 `if in.GetPageSize() > 200 { in.PageSize = 200 }`)。
4. **分页三处修复M6/M7**`fetch.go``pending.go``supply/fetch.go` 都调整为"先归一化 `page_no`/`page_size`,再计算 `Offset`"`pending.go:31``< 0` 改成与其它两处一致的 `< 10`;三处返回体把 `Count` 改成 `cnt`(已算好的总数)。
5. **审核语义修正M11**`approve.go:27-38``switch``default: return nil, errcode.ErrInvalidArgument`,并校验当前 `data.Approve == 0`(只允许对待审记录操作);如需留痕,用现有的 `remark` 或直接依赖 `updated_at`
6. **账号唯一性M12**:迁移里给 `market_agency.account``market_supply.account` 加唯一索引,并在 `create.go`/`supply/create.go` 建号前先 `Where("account = ?").First` 判重后返回明确的 `errcode`(避免仅靠索引报 500
7. **登录链路M4/M9**`login.go:53` 改用与校验侧一致的签发方式——参照 base/passport 的写法 `token.New(env.Runtime.JwtSecretKey).GenerateJwt(...)``module/base/passport/internal/logic/common/token.go:10`),不要用 `encipher.GenerateTokenAes`;同时落地 `verify` 字段的校验(`proto/agency.proto:41` 已声明为必填并对连续失败次数做限制。若暂时不做验证码至少对同账号失败次数计数Redis 已有初始化代码 `internal/impl/with.go:24-31`,可直接使用)。
8. **降 panic 面M5**:本模块侧给 `internal/server/new.go:23``grpc.NewServer()` 装带 `recover` 的 unary 拦截器根因SDK `crypto/token/jwt.go:68``err != nil` 时仍访问 `token.Claims`)建议对 SDK 提缺陷单——聚合入口的同类实现是安全的(`pkgs/all/internal/server/authorization.go:80``if err != nil || !tokenValue.Valid` 短路),可直接照抄这个判断顺序。
9. **日志与凭据M10/M14**:删除 `create.go:50,52``fmt.Println`;把 `internal/models/impl.go:83` 的无条件 `db.Debug()` 改为按配置开启(`internal/impl/with.go:41` 传入真实 `*types.SqlOptions`,或把 `Debug` 补进 `conf.DBConf`),生产不要打印含口令哈希的 SQL。
10. **收尾清理M15/M16/M17**`internal/server/new.go:26-30``Mux: gwRuntime.NewServeMux()`(需改生成模板 `protoc-gen-slc`,否则独立进程的 HTTP 网关一直不可用);`supply/create.go:34` 恢复 `Avatar` 赋值,并为 `Supply` 补登录 RPC 或明确删除其 `account`/`password` 字段;确认线上两张表 `password` 列无历史 MD5 存量(若有,需一次性迁移脚本 + 首次登录强制改密),之后删除两处 `salt` 列与 `set_password.go:47``"salt": ""` 写入;补一条集成测试覆盖"匿名/低权令牌不能 Approve、不能 Delete、不能读他人 Get/Single、列表 count 等于总数"。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。"统一鉴权框架""引入 DTO/VO""抽公共 base""改成 DDD/CQRS"一类改造不在此列;第 1、2、4、5 条都是恢复已有写法或改一行条件/一处归一顺序即可闭环。
2026-09-22 21:15:34 +08:00
## 9. 整改记录2026-09-22
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| M1 | 高 | `Approve` 无鉴权;`Agency.Create`/`Agency.Delete`/`Supply.Create` 鉴权被整段注释 | 已修复:恢复鉴权并补角色/归属校验,审核、开号、删号限定为有权限的角色 |
| M2 | 高 | `Get`/`Modify`/`SetPassword` 用请求 `identity` 覆盖 token `identity` | 已修复:一律以 `auth.Identity` 为准;请求参数与 token 不一致即返回 `ErrPermissionDenied` |
| M3 | 高 | `Data` 服务 5 个方法全为零实现却已注册对外 | 已修复:改为显式返回 `codes.Unimplemented`,不再返回成功 |
| M4 | 高 | 登录签发 AES 密文 vs 接口校验 HS256 JWT互不兼容 | 已修复:登录签发改用 SDK 同一套 token 签发函数,与接口侧校验格式统一,登录链路打通 |
| M5 | 高 | 非 JWT 的 `Authorization` 头触发 SDK `ParseJwt` 空指针、模块无 recover | 已修复:在 `internal/server/new.go` 增加最小 unary `recover` 拦截器 |
| M6 | 高 | 分页 `Offset` 早于参数归一化、`Pending``< 0` 归一导致 `LIMIT 0` | 已修复:先归一化分页(下限 1再计算 `Offset` |
| M7 | 高 | `Count` 被丢弃、`Total` 直接返回本页条数 | 已修复:改用独立 `Count` 查询得到总数 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。