Files
full/docs/market.md
2026-09-22 18:53:53 +08:00

36 KiB
Raw Blame History

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/maingRPC + HTTP Gateway 单进程)、cmd/cli(脚手架占位)、聚合入口 pkgs/allservice.Expose 复用共享 gRPC/Gateway
对外协议 gRPC + HTTP Gateway路径 `/market.<Agency
结论摘要 审核流程与账号生命周期完全未被保护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

  • Agencyproto/agency.proto:7-35):代理商(省代公司)登录、建号、增删改查、改密、待审核列表、审核;
  • Supplyproto/supply.proto:7-24):供应商建号、增删改查;
  • Dataproto/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.Newimpl.NewImplserver.New(nil)service.Newsrv.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/Verify12 行,唯一有单元测试的包)
internal/server/{new,agency_server,supply_server,data_server}.go 生成骨架:注册 3 个 gRPC service、按 service 转发到 logic
internal/logic/agency/*.go10 个) 登录/建号/查/列表/改/删/改密/待审/审核/字段映射
internal/logic/supply/*.go6 个) 供应商建号/查/列表/改/删/字段映射
internal/logic/data/*.go5 个) 概况/会员列表/会员详情/订单列表/订单详情(全部占位
service/expose.go / dependencies.go 聚合宿主注入(applyDependencies + 3 个 Handler 注册)
proto/*.proto 4 个契约文件(const.proto 为跨模块复制的 blocks 定义)
test/agency/*.httptest/supply/fetch.http 10 个手工请求脚本(非自动化测试)

依赖注入:service.Dependenciesservice/dependencies.go:12-17)非 nil 时覆盖 internal/impl 包级变量,聚合实例见 pkgs/all/internal/service/market.go:10-19;同样地,Dependencies.Cache 被声明但 applyDependencies:19-29)忽略。

3. 接口清单

全部接口注册在 grpc-gateway 的 POST 路由上Agencypb/agency.pb.gw.go:287,307,327,347,367,387,407,427,447Supplypb/supply.pb.gw.go:408-412Datapb/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-22member_fetch.go:26-28member_details.go:24-26order_fetch.go:26-28order_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 里的 OverviewReply4 个计数 + 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/maininternal/server/new.go:23grpc.NewServer() 无拦截器)下则是彻底匿名可用。Authorization.Anonymous 白名单文件不在仓库内(pkgs/all/etc/ 不存在)→【信息不足】无法确认聚合部署下哪些方法被列为匿名。

4. 数据模型与表

market_agencyinternal/models/market_agency.go:11-37GORM 自动迁移 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_supplyinternal/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 '' 头像 / 账号(CreateAvatar 被注释掉,见 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.2statusapprovecommission_rateagency_type 均无 check 约束;password 长度未按 bcrypt 输出60 字符)收紧到 60。

5. 核心流程

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:20fetch.go:22modify.go:20 都调用了;Login 不调用是合理的,因为它就是登录入口),只校验 identity 非空(:17-19)。任何人只要能把请求送到(独立部署时直连 gRPC 端口即可),就能把任意代理商 approve 改成 2通过:29)或 -2拒绝:34)。审核是登录的唯一前置条件(login.go:44),此接口失守等于渠道准入体系失守。
internal/logic/agency/create.go:20-23internal/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-27modify.go:25-27set_password.go:25-27 请求参数覆盖 token 身份:三处都是 identity = auth.Identity; if in.GetIdentity() != "" { identity = in.GetIdentity() },随后的查询/更新(get.go:29modify.go:45set_password.go:33)全部使用被覆盖后的 identity。任意已登录账号传一个他人的 identity 即可:Get 读出对方 id_name/证件照/phone/emailget.go:33-52 全字段返回)、Modify 改掉对方 account(登录名)、phoneemailcommission_rateorg_namemodify.go:28-45)。SetPassword 因需旧密码(:37)危害较小,但仍可对任意目标发起撞库式改密。
internal/logic/agency/fetch.go:16-52pending.go:16-47 列表接口只验"token 有效"、不验角色、不按归属过滤fetch.go:22pending.go:17 都是 service.ParseMetaCtx(ctx, nil),无任何 Where 限定调用方。任何持合法 JWT 的调用方(含商城会员等其他业务域角色;独立进程部署下连 JWT 都不需要)都能一次拉走全部代理商(含待审核)的 phone/email/id_name/证件照 URLfetch.go:30-32 只把 page_size < 10 归一到 50、没有上限,传 page_size=1000000 即全量导出。
internal/logic/agency/login.go:53 + SDK crypto/token/jwt.go:64-73golang-jwt/jwt/v5@v5.3.1/parser.go:138-140 登录签发的令牌无法被任何接口接受,且会触发空指针 panicLoginencipher.GenerateTokenAes 生成的是 AES-CBC 密文的 base64SDK crypto/encipher/encipher.go:29-54),而 service.ParseMetaCtx 走的是 HS256 JWT 解析SDK service/meta.go:31token.ParseJwt。base64 密文里没有点号,jwt.ParseWithClaims 在第一段就返回 nil, ErrTokenMalformedparser.go:139-140而 SDK 随后执行 if claims, ok := token.Claims.(*Claims); ...jwt.go:68在 nil 上取字段 → 空指针 panic;模块 internal/server/new.go:23grpc.NewServer() 未装 recover 拦截器goroutine panic 会终止进程。两个后果:① 合法登录的代理商调用任何受保护接口都失败(聚合入口的 authorization.validate 同样只认 JWTpkgs/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_logfileetc/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-32modify.go:46-47supply/get.go:23-26supply/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.GenerateFromPasswordDefaultCost调用点只有 create.go:27supply/create.go:27set_password.go:41login.go:34,全仓库无 md5 引用;internal/password/password_test.go:5-13 覆盖了正确/错误口令两个分支。遗留形态是数据侧market_agency.saltmodels/market_agency.go:18)与 market_supply.saltmodels/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/SourceSDK 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_nopage_size 缺省 0Offset 恒为 0 → 无论要第几页都返回第一页数据;② 传 page_no=3,page_size=5Offset=10LIMIT 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):38GORM 对 Limit(0) 生成 LIMIT 0gorm.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,50pending.go:38,45supply/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() 只处理 1approve=2:29)和 2approve=-2:34其它值(含缺省 0、或任意数字什么都不做却返回 Code:0, Message:"OK",调用方会误判审核成功。函数还不校验当前状态(可对已通过/-2 的记录反复改判)、不记录审核人与审核时间(无审计字段),返回体也没有 Timeseq(对比 delete.go:35)。
internal/logic/agency/ref.go:16-35 列表字段映射漏掉 Approveproto/agency.proto:65 定义了该字段):/market.Agency/Fetch/Pending 返回的 approve 恒为空,前端拿不到审核状态,Pending 的结果也无法体现"待审 vs 已拒绝"。supply/ref.go:14-29 同样漏了 ApproveCommissionRatesupply/get.go:28-41 也漏了这两个字段)。
internal/logic/agency/modify.go:28-45,53 Updates(&mktModel) 传结构体 → GORM 跳过零值:commission_rate 改不回 0、remark/email 等无法清空;Account(登录名)可被任意改;in.Passwordproto/agency.proto:52)被静默忽略。同时 :53Identity: mktModel.Identity 取的是刚构造的入参结构体:28-44 从未赋值该字段)→ 响应里的 identity 恒为空字符串,调用方拿不到被改对象标识。supply/modify.go:38-46 同一问题(返回的 Identity 也是空)。
internal/logic/agency/create.go:24-49supply/create.go:24-44 不校验 account 唯一性,而 account 列没有唯一索引(models/market_agency.go:15models/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 创建时忽略请求里的 statusapprove,硬编码 Status: 1approve 取零值 0。approve=0 符合"待审核"语义,但 proto/agency.proto:61 声明的 status 被静默丢弃,接口契约与行为不符。
internal/logic/agency/fetch.go:35pending.go:36 tx.Where(...) 未回写到 tx,而同函数的 :38/:41Fetch:34Pending都写了同一函数内两种写法并存。当前依赖 GORM "clone=0 时共享 Statement" 的内部行为仍能生效,属隐患而非现网故障,但版本升级或链式顺序调整后会静默丢条件。
internal/logic/supply/get.go:23 若请求 identity 为空则生成 Where("identity=''"),直接返回 GORM ErrRecordNotFound 原始错误;supply.Modify 有非空校验(modify.go:22-24supply.Get 没有,同目录口径不一致。
internal/models/query.go:4-7 NormalStatus 常量无任何引用(只有 DisabledStatuslogin.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-39total_approve/total_month/total_all/total_staffreport_month/report_staff 永远为空。
internal/logic/data/member_fetch.go:26-28member_details.go:24-26order_fetch.go:26-28order_details.go:24-26 会员列表、会员详情、订单列表、订单详情 4 个接口同样是"只做鉴权(+分页参数归一)+ TODO + return"的骨架,reply 从未赋值。market.Data 服务的 5 个方法零实现,接口已对外注册(internal/server/data_server.go:19-41service/expose.go:26-28),对调用方表现为"永远返回空"。
internal/logic/data/member_fetch.go:19-24order_fetch.go:19-24 占位实现里归一化出的 PageNo/PageSize 无任何消费者(没有查询),且用 < 10 → 50pending.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/Deletesupply/create.go:34 还把 Avatar 的赋值注释掉了(字段永远为空)。供应商账号目前只能建、不能用。
internal/logic/agency/create.go:19-23delete.go:16-21supply/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 都是 postgresetc/market_dev.yaml:5market_prod.yaml:5market_test.yaml:5)→ 全部 SQL 与参数打印,包含代理商 password 哈希、id_name、证件照 URL、phoneemail
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 把它nilGatewayMux 传给 SDKSDK service.Start 用它执行 http.ListenAndServe(httpAddr, s.Opts.GatewayMux)SDK service/service.go:106-108)→ 非空接口持有 nil 指针,(*ServeMux).ServeHTTP 首次访问 s.unescapingModegrpc-gateway runtime/mux.go:407-420)即空指针 panicnet/http recover 后断开连接。只有聚合入口 pkgs/all 自建 ServeMuxpkgs/all/internal/server/server.go:38)才正常。同一缺陷存在于全部模块的生成骨架,属生成器级问题。
internal/impl/impl.go:9internal/impl/with.go:19-21 RedisCacheEtcd 初始化后无任何使用点(死代码);service/dependencies.go:16Cache 字段被 applyDependencies:19-29)忽略。
全模块 只有 internal/password/password_test.go:5-13 一个单元测试;internal/logic/** 无测试、internal/models 无测试。本次发现的分页错位、LIMIT 0count 语义、审核空转switch 无 default 分支)等问题都无回归保护。
internal/logic/agency/{login.go:53,create.go:50} 使用标准库 log/fmt.Println/fmt.Errorf 与 SDK printer 混用(fetch.go:44delete.go:27printer.Error),日志出口与格式不统一;fmt.Println 的调试输出被直接留在生产路径上。
etc/market_dev.yaml:20etc/market_prod.yaml:19etc/market_test.yaml:19 MicroService.Anonymous 三个环境不一致dev 是 market.ping.helloprod/test 是 mall.ping.hello),且本模块没有任何 ping 方法proto/*.proto 只有 Agency/Supply/Data 三个服务)→ 该白名单条目恒为无效配置;SecretKey: CHANGE_MEpassword=CHANGE_ME 也是占位值。

7. 风险汇总

编号 级别 问题 影响面
M1 Approve 无任何鉴权;Agency.Create/Agency.Delete/Supply.Create 鉴权被注释(approve.go:15-46create.go:20-23delete.go:17-21supply/create.go:19-23 渠道准入体系可被绕过、账号可被自助创建与删除
M2 Get/Modify/SetPassword 用请求 identity 覆盖 token identityget.go:25-27modify.go:25-27set_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 0fetch.go:19,27-32,43pending.go:31,38supply/fetch.go:19,27-32,37 列表接口返回错页或恒空
M7 Count 被丢弃、返回本页条数(fetch.go:43,50pending.go:38,45supply/fetch.go:37,43 前端分页计算错误
M8 列表接口仅验 token、无角色/归属过滤、page_size 无上限(fetch.go:22,30-32pending.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:15create.go:24-49login.go:26 登录串号、账号抢占
M13 列表映射漏 ApproveModify 返回空 identityref.go:16-35modify.go:53supply/modify.go:46 前端拿不到审核状态与变更对象标识
M14 生产强制 SQL Debug + 启动 AutoMigrateinternal/models/impl.go:39,83 敏感信息落盘、DDL 不可控
M15 独立进程 HTTP 网关因 Mux 未赋值不可用(internal/server/new.go:16cmd/main/main.go:32 可用性(生成器级、全模块共有)
M16 供应商有密码字段却无登录接口、Avatar 赋值被注释(proto/supply.proto:7-24supply/create.go:34 功能不可用
M17 salt 列残留 + 无历史哈希兼容分支;死代码、无测试、cmd/cli/README 占位 存量账号登录(【信息不足】)、可维护性

8. 修复建议(务实项)

  1. 补齐四处鉴权M1最高优先approve.go 开头补 auth, err := service.ParseMetaCtx(ctx, nil)(它还需要一个"审核人"概念,可先用返回值做非空校验);create.go:20-23delete.go:17-21supply/create.go:19-23 把注释掉的段落恢复。这样至少保证"必须持合法令牌",与同目录其它接口一致。
  2. 禁止 identity 覆盖M2get.go:25-27modify.go:25-27set_password.go:25-27 统一改为只使用 auth.Identity;若确实需要"管理员代改",就显式判定 auth 的某个角色扩展字段(claims.ExtendSDK service/meta.go:51-60),未带该角色时直接 errcode.ErrPermissionDenied
  3. 列表接口收口M8/M13fetch.go:22pending.go:17ParseMetaCtx(ctx, nil) 改为传入角色要求;ref.go:16-35 补上 Approvesupply/ref.goApprove/CommissionRatefetch.go:30-32 增加上界(例如 if in.GetPageSize() > 200 { in.PageSize = 200 })。
  4. 分页三处修复M6/M7fetch.gopending.gosupply/fetch.go 都调整为"先归一化 page_no/page_size,再计算 Offset"pending.go:31< 0 改成与其它两处一致的 < 10;三处返回体把 Count 改成 cnt(已算好的总数)。
  5. 审核语义修正M11approve.go:27-38switchdefault: return nil, errcode.ErrInvalidArgument,并校验当前 data.Approve == 0(只允许对待审记录操作);如需留痕,用现有的 remark 或直接依赖 updated_at
  6. 账号唯一性M12:迁移里给 market_agency.accountmarket_supply.account 加唯一索引,并在 create.go/supply/create.go 建号前先 Where("account = ?").First 判重后返回明确的 errcode(避免仅靠索引报 500
  7. 登录链路M4/M9login.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:23grpc.NewServer() 装带 recover 的 unary 拦截器根因SDK crypto/token/jwt.go:68err != nil 时仍访问 token.Claims)建议对 SDK 提缺陷单——聚合入口的同类实现是安全的(pkgs/all/internal/server/authorization.go:80if err != nil || !tokenValue.Valid 短路),可直接照抄这个判断顺序。
  9. 日志与凭据M10/M14:删除 create.go:50,52fmt.Println;把 internal/models/impl.go:83 的无条件 db.Debug() 改为按配置开启(internal/impl/with.go:41 传入真实 *types.SqlOptions,或把 Debug 补进 conf.DBConf),生产不要打印含口令哈希的 SQL。
  10. 收尾清理M15/M16/M17internal/server/new.go:26-30Mux: 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 条都是恢复已有写法或改一行条件/一处归一顺序即可闭环。