Files
full/docs/audit/module-ec-market.md
yanweidong 63aeedc2fe docs: add per-module audit reports (18 modules)
Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
2026-09-14 22:16:27 +08:00

552 lines
59 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.
# 审计报告module/ec/market
> 审计对象:`D:\work\bsm-infra\full\module\ec\market`(代理/供应商市场:`Agency`、`Supply`、`Data` 三个 gRPC 服务)
> 审计日期2026-08基于当前工作区快照
> 静态检查:`gofmt -l .` 无输出(格式合规);`go vet ./...` 退出码 0无告警
## 1. 模块概览
| 维度 | 内容 |
|---|---|
| 模块路径 | `module/ec/market`Go module `bsm/full/module/ec/market``go 1.26.5`,已加入 `go.work` |
| 对外服务 | `market.Agency`9 方法)、`market.Supply`5 方法)、`market.Data`5 方法),见 `proto/agency.proto``proto/supply.proto``proto/data.proto` |
| 数据模型 | `market_agency``market_supply` 两张表(`internal/models/market_agency.go``market_supply.go`),均内嵌 SDK 的 `types.Std_IICUDS`(自增 ID、唯一 identity、软删除、status |
| 主要业务 | 代理商注册Create、登录Login、审核Pending/Approve、资料维护Get/Modify/Delete/SetPassword/Fetch供应商资料增删改查 |
| 部署形态 | ① 独立进程 `cmd/main``etc/market_dev|test|prod.yaml`);② 聚合网关 `pkgs/ecmall``pkgs/all` 通过 `service.Expose()` 内嵌(`service/expose.go:18-32` |
| 密码实现 | `internal/password/password.go` 使用 bcrypt`bcrypt.DefaultCost`),无 MD5 残留(全模块 grep 无 `md5` |
| 鉴权实现 | 业务代码依赖 SDK `service.ParseMetaCtx`;聚合部署另有 HS256 JWT 一元拦截器 `pkgs/ecmall/internal/server/authorization.go` |
| 测试 | 仅 `internal/password/password_test.go` 1 个单测;`test/{agency,supply}/*.http` 10 个手工样例 |
| README | `module/ec/market/README.md` 仅 3 行(`# market` / `market agency`),无接口、权限、迁移说明 |
## 2. 审计范围与方法
**已完整阅读38 个非 pb Go 文件,约 40 KB100% 覆盖)**
- 入口与装配:`cmd/main/main.go``cmd/cli/main.go``internal/config/config.go``internal/impl/impl.go``internal/impl/with.go``internal/server/{new,agency_server,supply_server,data_server}.go``service/{expose,dependencies}.go`
- 业务逻辑:`internal/logic/agency/*`approve、create、delete、fetch、get、login、modify、pending、ref、set_password 共 10 个)、`internal/logic/supply/*`create、delete、fetch、get、modify、ref 共 6 个)、`internal/logic/data/*`5 个)
- 模型与密码:`internal/models/{impl,market_agency,market_supply,query}.go``internal/password/{password.go,password_test.go}`
- 契约与配置:`proto/{agency,supply,data,const}.proto``etc/market_{dev,test,prod}.yaml``etc/supervisor.bsm-ec-market.conf``test/agency/*.http`9 个)、`test/supply/fetch.http``README.md`
- SDK`sdk/typescript/agency.ts``market/index.ts``market/const.ts` 全读;`sdk/typescript/{blocks,data}.ts` 抽样(消息定义与方法签名,未逐行读 918/470 行生成代码)
**为判定结论而额外核对的依赖(非审计目标)**
- `D:\work\bsm-sdk\core``crypto/encipher/encipher.go``GenerateTokenAes` 实现)、`crypto/token/jwt.go``ParseJwt`)、`service/meta.go``ParseMetaCtx`)、`types/db.go``Std_IICUDS` 软删除/唯一索引)、`conf/types.go``env/env.go``service/service.go`
- 聚合部署:`pkgs/ecmall/internal/server/{authorization,dynamic,server}.go``pkgs/ecmall/internal/{config/config.go,impl/impl.go,service/market.go}``pkgs/ecmall/etc/default_dev.yaml``pkgs/all/internal/server/authorization.go`
- `gorm.io/gorm@v1.31.2` 源码:`gorm.go``getInstance()``chainable_api.go``Where/Limit/Offset``clause/limit.go``finisher_api.go``Count`(用于排除/确认链式调用与分页边界行为)
- `github.com/golang-jwt/jwt/v5@v5.3.1``parser.go` 畸形 token 的返回值(用于确认 panic 可达性)
- 既有基线文档:`wiki/audit-2026-08-10.md``wiki/api/10-market.md`、根 `README.md`(第 156-162 行)
**方法与边界**
- 只读审计:未修改、格式化、新建或删除任何 `.go/.proto/.yaml/.ts/go.mod`;唯一写入为本文件。
- `pb/` 下 11 个生成文件未逐行阅读(仅 `pb/blocks_compat.go` 全读,其余通过 grep 核对类型与字段号)。
- **无法验证**:真实数据库 schema 与索引(仓库无 DDL/迁移脚本,`AutoMigrate` 在运行时生成)、线上历史数据是否存在 MD5 哈希、生产配置实际值、`pkgs/all` 的实际启用服务集合、运行时行为未启动服务、未跑集成测试、SDK `env.Runtime.JwtSecretKey` 在生产容器的真实取值。
- 严重级别口径P0 可被利用的安全漏洞/必然数据损坏P1 高概率线上故障、越权、性能瓶颈P2 可维护性/健壮性/可观测性P3 风格/文档/一致性。
## 3. 问题清单
### P0
#### 1. 代理商管理接口完全缺少授权(角色/归属)校验:任意已登录用户可自审、删除、篡改任意代理商
- **位置**`module/ec/market/internal/logic/agency/approve.go:15-38``module/ec/market/internal/logic/agency/delete.go:16-29``module/ec/market/internal/logic/agency/modify.go:20-27``module/ec/market/internal/logic/agency/get.go:20-27``module/ec/market/internal/logic/agency/fetch.go:22-25``pkgs/ecmall/internal/server/authorization.go:42-53``pkgs/ecmall/etc/default_dev.yaml:34`
- **证据**
```go
// approve.go:15-21 —— 无 ParseMetaCtx、无角色校验identity 直接取自请求体
func Approve(ctx context.Context, in *pb.ApproveRequest) (reply *pb.IdentityStatusReply, err error) {
var data *models.MarketAgency
if in.GetIdentity() == "" { return nil, errcode.ErrInvalidArgument }
if err := impl.DBService.Where("identity = ?", in.GetIdentity()).First(&data).Error; err != nil {
```
```go
// 聚合网关注入的鉴权只校验"签名 + 过期",不校验角色/归属authorization.go:74-79
tokenValue, err := jwt.ParseWithClaims(raw, claims, func(tokenValue *jwt.Token) (any, error) {
if tokenValue.Method != jwt.SigningMethodHS256 { ... }
return a.key, nil
}, jwt.WithExpirationRequired(), jwt.WithIssuedAt(), ...)
```
```go
// market 侧唯一的"鉴权"是 ParseMetaCtx(ctx, nil)opts 为 nil 时不校验角色SDK service/meta.go:36-45
if opts != nil { if !checkRole(claims, "role", opts.RoleValue) { return nil, errcode.ErrPermissionDenied } }
```
- **影响**:聚合部署里匿名白名单只有 `/market.Agency/Login``default_dev.yaml:34`),因此"已登录"即可通过;而全站共用一个 HS256 密钥(`pkgs/ecmall/internal/config/config.go:79``Authorization.Key` 赋给 `env.Runtime.JwtSecretKey`passport 会员登录签发的普通会员 token`module/base/passport/internal/logic/common/token.go:9-14`)同样被接受。结果是**任意 C 端会员 token 即可**:① 用 `Approve{approve:1, identity:<自己>}` 自审通过(自审,无审批人概念);② `Delete` 删除任意代理商的记录(`RowsAffected` 不看,软删除);③ `Modify` 改写任意代理商的 `commission_rate`(直接关联结算金额)、手机号、证件照片;④ `Fetch` 拉取全量代理商含身份证正反面与手机号。这是跨租户越权 + 资金字段篡改,属于可直接利用的授权缺失。
- **建议**:① `Approve/Delete/Modify(管理语义)/Fetch/Pending` 必须显式传入 `ParseMetaCtx(ctx, &service.ParseOptions{RoleValue:"admin"})` 或等价的作用域校验,并在 market 域名内建立角色/权限模型(当前 `market.Agency/Login``extend` 为空 map无法表达角色`agency/login.go:53`);② 自助类接口(`Get/Modify/SetPassword`)忽略请求体 identity强制使用 `claims.Identity`;③ 聚合层白名单保持最小集合并对 market 管理方法加角色白名单;④ 为"谁能审批"补充管理员表与审计字段。
#### 2. 代理商登录签发的 token 不是 JWTAES-CBC 密文),与全站校验方式不兼容 → 登录后所有受保护接口不可用
- **位置**`module/ec/market/internal/logic/agency/login.go:53-56``D:\work\bsm-sdk\core\crypto\encipher\encipher.go:29-54,74``D:\work\bsm-sdk\core\crypto\token\jwt.go:64-73``pkgs/ecmall/internal/server/authorization.go:74-79`
- **证据**
```go
// login.go:53 —— 用 encipherAES生成 token
token, err := encipher.GenerateTokenAes(marketData.ID, marketData.Identity, "", "", market, map[string]string{})
```
```go
// SDK encipher.go:44-53 —— 实际是 AES-CBC 加密 JSON 再 base64不是 JWT
byte, err := json.Marshal(claims)
token, err := AesEncryptCBC(byte) // base64(AES-CBC(json))
return token, nil
```
```go
// 而校验方要求 JWTSDK token/jwt.go:65-68 与聚合拦截器 authorization.go:74-79 都用 jwt.ParseWithClaims + HS256
token, err := jwt.ParseWithClaims(tokenstring, &Claims{}, func(token *jwt.Token) (any, error) { return []byte(t.SecretKey), nil })
```
- **影响**:① 代理商 `Login` 成功后拿到的 token 无法通过聚合网关(`jwt.WithValidMethods([HS256])` 直接判为格式错误)也无法通过 market 自己的 `ParseMetaCtx`,即**登录接口事实上不可用**,登录后的任何调用都失败;② 反过来,能通过校验的只有其他域签发的 JWTpassport 会员/mgt 管理员),使问题 1 的利用门槛降到"任意注册会员";③ 两处"GenerateTokenAes"同名不同实现passport 的是 JWTmarket/encipher 的是 AES极易误用。对同一密钥还使用 `JwtSecret[:blockSize]` 作为固定 IV`encipher.go:69`),密文可确定性重放。
- **建议**`agency.Login` 改用 `token.New(env.Runtime.JwtSecretKey).GenerateJwt(...)`(或 passport 同款封装),与 `ParseMetaCtx`/聚合拦截器统一算法;删除或私有化 `encipher.GenerateTokenAes` 的 AES 分支;登录响应增加 `expires_at`;上线前用一条端到端用例(登录 → 带 token 调 `Agency/Get`)做回归门禁。
### P1
#### 3. 登录无防爆破、无验证码校验、错误码可枚举账号
- **位置**`module/ec/market/internal/logic/agency/login.go:22-46``module/ec/market/proto/agency.proto:38-42``D:\work\bsm-sdk\core\errcode\errcode.go:28,66`
- **证据**
```go
// login.go:26-36 —— 直接查库比密码,无失败计数/锁定/限流proto 里 "验证码 必填" 的 verify 从未被读取
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errcode.ErrAccountNotFound } ... }
if !password.Verify(marketData.Password, in.Password) { return nil, errcode.ErrPassword }
```
```proto
// proto/agency.proto:41
string verify = 3; // 检验码 必填
```
- **影响**:① 账号不存在返回 `ErrAccountNotFound`(404 "Account")、密码错返回 `ErrPassword`(1108),是标准的账号枚举 oracle② 无限流/失败锁定/延时bcrypt 校验虽慢但仍可离线+在线爆破(在线爆破同时造成 CPU 放大);③ `verify` 字段被契约声明为必填却完全不校验,前端/客户端可能以为已有验证码保护;④ 无登录失败审计,无法发现撞库。
- **建议**:统一返回"账号或密码错误";引入 Redis模块已初始化 `impl.RedisCache` 但零使用)做按账号+IP 的失败计数与指数退避;接入真实图形/短信验证码并在服务端校验 `verify`(含一次性消费与过期);记录成功/失败登录审计账号、IP、UA、结果
#### 4. 资料读取/修改/改密接口越权IDOR请求体 identity 优先于 token 身份
- **位置**`module/ec/market/internal/logic/agency/get.go:20-32``agency/modify.go:20-27``agency/set_password.go:24-39`
- **证据**
```go
// get.go:24-29modify.go:24-27、set_password.go:24-26 同型)
identity = auth.Identity
if in.GetIdentity() != "" { identity = in.GetIdentity() } // 请求体覆盖 token 身份
mktModel := models.MarketAgency{}
if err := impl.DBService.Where("identity=?", identity).First(&mktModel).Error; err != nil {
```
```go
// set_password.go:33-39 —— 未校验 identity 是否等于调用者
if err := impl.DBService.Where("identity = ?", identity).First(&agency).Error; err != nil { ... }
if !password.Verify(agency.Password, in.GetOldPassword()) { return nil, errcode.ErrPassword }
```
- **影响**:任意登录用户传入他人 identity 即可:读取他人完整档案(手机号、`id_before/id_after` 身份证照片 URL、机构照、邮箱、地区篡改他人姓名/账号/手机号/证件照片/佣金比例;在已知他人旧密码的前提下重置他人密码(横向改密)。`Modify` 还接受 `account`,可用于抢占/混淆他人登录账号。
- **建议**:自助接口删除 `in.Identity` 覆盖分支,只允许操作 `claims.Identity`;管理侧另开显式接口并做角色校验;`SetPassword` 增加旧密码错误计数与新密码强度校验(当前仅校验非空,无长度/复杂度要求,`set_password.go:28-30`)。
#### 5. 列表分页与过滤缺陷:总数返回当前页长度、`Pending` 默认 `page_size=0` 恒返回空、`page_size` 无上限、`keyword/params` 被忽略、列表回吐全量敏感资质
- **位置**`module/ec/market/internal/logic/agency/fetch.go:19,27-32,43-51``agency/pending.go:24,31-33,38-46``internal/logic/supply/fetch.go:19,30-32,37-44``agency/ref.go:16-35``proto/const.proto:55-63`
- **证据**
```go
// fetch.go:43-51 —— 查了 cnt 却用 len(data) 当总数
if err := tx.Order("created_at desc").Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil { ... }
return &pb.AgencyReply{ Data: ref(data), Count: int64(len(data)) }, nil
```
```go
// pending.go:31-38 —— 守卫写成 <0proto 默认 0 时 Limit(0) 生成 "LIMIT 0",列表恒空
if in.GetPageSize() < 0 { in.PageSize = 50 }
tx := impl.DBService.Model(&models.MarketAgency{}).Where("approve = 0").Order("created_at desc")
if err := tx.Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil {
```
```go
// ref.go:16-27 —— 列表逐条回吐手机号与证件照片
row := pb.MarketAgenctyItem{ Identity: in.Identity, Name: in.Name, ... Phone: in.Phone, OrgPhoto: in.OrgPhoto, IdName: in.IDName, IdBefore: in.IDBefore, IdAfter: in.IDAfter, ... }
```
- **影响**:① 前端无法分页(`count` 恒等于本页条数);② 待审核列表在默认参数下永远是空数组,审核台"看不到待审";③ `page_size` 只有下限没有上限(`fetch.go:30-32``<10` 强制 50极大值原样下传`page_size=10^9` 可一次拉全表 → 内存/带宽放大;④ `MarketFetchRequest.keyword``params` 在代码中从未使用,前端按关键词搜索会静默返回全量数据(易被误当"搜索无结果/结果错误");⑤ 列表接口把身份证、手机号等敏感资质批量回吐给任何具备读权限者,属最小权限违规。(注:`Offset``in.PageNo` 兜底之前计算,`page_no=0` 时得到负偏移;经核对 gorm `clause/limit.go:20,40-42`,负偏移被静默归一为 0因此后果是"回到第一页"而非报错,仅记录。)
- **建议**`Count``cnt`,并区分"本页条数/总数"字段;`pending.go` 守卫改为 `pageSize < 1`(或复用同一 `normalizePage` 函数);统一 `pageSize` 上限(如 100并在协议层校验实现 `keyword``name/account/phone/org_name` LIKE`params` 白名单;列表返回裁剪字段(不回吐 `id_before/id_after` 原图 URL改签名 URL 或详情页鉴权下载);为 `created_at` 增加索引以支撑排序。
#### 6. 历史 MD5 账号被彻底锁死:无法登录、无法改密,且系统没有任何密码重置流程
- **位置**`module/ec/market/internal/logic/agency/set_password.go:37-39``internal/password/password.go:10-11``module/ec/market/proto/agency.proto:26-27``wiki/audit-2026-08-10.md:27`
- **证据**
```go
// set_password.go:37 —— 改密必须通过 bcrypt 校验旧密码
if !password.Verify(agency.Password, in.GetOldPassword()) { return nil, errcode.ErrPassword }
```
```go
// password.go:10-11 —— 只有 bcrypt无 MD5 兼容分支(全模块无 md5 引用)
func Verify(hash, plain string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil }
```
```markdown
wiki/audit-2026-08-10.md:27 market 和 mall 的账号创建、登录、改密及初始化密码已从 MD5 加盐切换为 bcrypt。历史 MD5 记录需要通过密码重置流程迁移。
```
- **影响**MD5 时代产生的 `market_agency.password` 旧行:登录因 `bcrypt.CompareHashAndPassword` 解析失败被拒;`SetPassword` 又必须先通过 bcrypt 校验旧密码 → **死锁**,只能人工改库。`Agency` 服务9 个方法)中不存在"忘记密码/重置密码"RPCwiki 声称的"密码重置流程迁移"在 market 侧并未实现(`internal/models/market_agency.go:18` 仍保留无用的 `Salt` 字段,`set_password.go:47` 每次改密显式把它清空,说明迁移只做了一半)。是否线上仍有历史 MD5 行无法在本工作区验证。
- **建议**:上线前统计 `password` 字段不以 `$2a$/$2b$` 开头的行数;为 `Login` 增加一次性兼容分支MD5+salt 校验成功即用 bcrypt 重写该行并记录审计),或提供管理员触发的一次性重置接口 + 短信/邮箱验证的重置流程;迁移完成后下线兼容分支与 `Salt` 列;同步修订 wiki 描述,避免"已完成迁移"的错误结论。
#### 7. 账号唯一性缺失:`account/phone` 无唯一索引、注册无重复校验、无并发保护
- **位置**`module/ec/market/internal/models/market_agency.go:15-16``internal/logic/agency/create.go:24-55``internal/logic/agency/login.go:26`
- **证据**
```go
// market_agency.go:15-16 —— Account/Phone 均无 uniqueIndex对比 SDK types/db.go:30 的 Identity 有 uniqueIndex
Account string `gorm:"column:account;type:varchar(255);default:'';" json:"account"`
Phone string `gorm:"column:phone;type:varchar(20);not null;" json:"phone"`
```
```go
// create.go:24-26,51 —— 只校验非空,不查重,非事务,插入失败原样返回 DB 错误
if in.GetName() == "" || in.GetAccount() == "" || in.GetPassword() == "" { return nil, errcode.ErrInvalidArgument }
...
if err := impl.DBService.Create(&mktModel).Error; err != nil { fmt.Println("err = ", err); return nil, err }
```
```go
// login.go:26 —— 无 ORDER BY 的 First重复账号时取哪一行由数据库决定
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
```
- **影响**:同一 `account` 可被并发或重复注册多行(`market_supply` 同样,且其 Create 连 account/password 都不校验,见问题 17随后登录可能命中非本人行 → 合法用户被拒登或密码校验对象错位;结合问题 1 的任意 token 可 `Create`,攻击者可批量注册同名账号制造登录歧义/DoS。注册本身也没有幂等键客户端重试即产生重复主体重复的代理商主体会影响佣金与结算归属
- **建议**:迁移中为 `account` 建唯一索引(`uk_market_agency_account`)、`phone` 视业务建唯一或普通索引;`Create` 前置 `SELECT ... FOR UPDATE`/唯一约束兜底并把冲突映射为明确错误码;注册请求加幂等键(`request_id`)与频率限制;`account` 做规范化trim + 小写)后再落库与比对;`id<>0``Delete``id = ? or identity = ?` 属可疑遗留条件,需确认是否存在 id=0 的历史/种子行后清理。
#### 8. 审核与账号状态流转缺陷:无审批审计、通过后换证不重审、非法入参静默成功、无禁用/启用通道
- **位置**`module/ec/market/internal/logic/agency/approve.go:27-44``agency/modify.go:28-48``agency/create.go:32``internal/models/query.go:5-7``market_agency.go:30``proto/agency.proto:61,65,81`
- **证据**
```go
// approve.go:27-38 —— 无 default 分支;写库只改 approve无审批人/时间/意见;且无当前状态流转校验
switch in.GetApprove() {
case 1: if err := impl.DBService.Model(&data).Update("approve", 2).Error; err != nil { ... }
case 2: if err := impl.DBService.Model(&data).Update("approve", -2).Error; err != nil { ... }
}
```
```go
// modify.go:28-45 —— 可整体替换 id_name/id_before/id_after/org_photo却不重置 approve已通过者换证后仍显示"认证成功"
mktModel := &models.MarketAgency{ Name: in.GetName(), ..., IDBefore: in.GetIdBefore(), IDAfter: in.GetIdAfter(), ... }
if err := impl.DBService.Where("identity=?", identity).Updates(&mktModel).Error; err != nil {
```
```go
// create.go:32 —— Status 恒为 1models/query.go:6-7 的 DisabledStatus(-1) 分支在 login.go:39 永远不可达
Std_IICUDS: types.Std_IICUDS{Identity: utils.ULID(), Status: 1},
```
```proto
// proto/agency.proto:81请求 vs :65实体 —— 同一数字 2 在请求里表示"拒绝"、在实体里表示"审核通过"
int32 approve = 1; // 1:通过 2:拒绝
int32 approve = 20;// 审核状态0待审核 2审核通过 -2审核未通过
```
- **影响**:① `approve` 传 0/3/其它值时 switch 无命中,接口仍返回 `Code:0, Message:"OK"`(静默成功),前端会显示"审核成功"但数据未变;② 已通过approve=2的代理商可被再次置为 -2或反之缺少合法流转矩阵与拒绝原因落库③ 审核通过后 `Modify` 可任意替换证件照片且不回流审核状态,等于"先批后换资质"绕过审核;④ market 提供的接口中没有任何一个能设置 `status=-1``Modify` 未映射 status`Create` 硬编码 1proto 无 SetStatus RPC禁用能力与 `login.go:39` 的禁用校验形同虚设;⑤ 审批操作无审计(谁、何时、依据)——配置里的 `OpLog` 也不生效(见问题 15合规上无法追溯审核。
- **建议**定义显式状态机approve ∈ {0,2,-2}status ∈ {1,-1})与允许的迁移表,非法迁移返回明确错误;`Approve` 记录审批人 `claims.Identity`、时间、意见/原因(`ApproveRequest` 增加 `reason`),拒绝必填原因;`Modify` 变更证件类字段时把 approve 重置为 0 并留变更快照;新增启用/禁用接口(仅管理员);审批动作写业务操作日志(含前后值)。
#### 9. `Data` 服务 5 个 RPC 在未实现的情况下以"成功空响应"对外暴露
- **位置**`module/ec/market/internal/logic/data/overview.go:18-23``data/member_fetch.go:26-28``data/member_details.go:24-26``data/order_fetch.go:26-28``data/order_details.go:24-26``internal/server/new.go:34``wiki/audit-2026-08-10.md:35`
- **证据**
```go
// overview.go:11-23 —— 具名返回值 + 裸 returnreply 与 err 都保持零值nil, nilRPC 表现为"成功但空"
func Overview(ctx context.Context, in *pb.Empty) (reply *pb.OverviewReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
if err != nil { return nil, err }
// TODO: valid code
// TODO: add your logic code & delete this line.
return
}
```
```go
// member_fetch.go:11-28 —— 同型;其余 member_details.go:24-26、order_fetch.go:26-28、order_details.go:24-26 一致
func MemberFetch(ctx context.Context, in *pb.MarketFetchRequest) (reply *pb.DataReply, err error) {
...
// TODO: add your logic code & delete this line.
return
}
```
```go
// server/new.go:34 —— 仍然注册给 gRPC客户端看到服务"存在"
pb.RegisterDataServer(srv.Grpc, NewDataServer())
```
- **影响**:调用方拿到 `err == nil` 与空消息HTTP 侧 `{}`),无法区分"未实现"与"确实没有数据";经营看板会把空值当 0/无数据展示,属静默失败,比返回 `Unimplemented` 危险得多(`wiki/audit-2026-08-10.md:35` 已把同类问题标为 P1。同时这些占位方法仍消耗 `ParseMetaCtx` 校验与参数校验,掩盖了真实可用接口清单。
- **建议**:占位实现一律 `return nil, errcode.ErrUnimplemented`;在 proto 注释与 `wiki/api/10-market.md` 标注未实现;若短期不交付,则从注册与 SDK 中下架 `Data` 服务,避免客户端按"可用"集成。
#### 10. 性能:登录热路径按未加索引的 `account` 查询、全模块零缓存
- **位置**`module/ec/market/internal/logic/agency/login.go:26``internal/models/market_agency.go:15``internal/impl/with.go:19-31`
- **证据**
```go
// login.go:26 与 market_agency.go:15 —— 查询列无索引Std_IICUDS 只为 id/identity/status/deleted_at 建索引)
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
Account string `gorm:"column:account;type:varchar(255);default:'';" json:"account"`
```
```go
// with.go:19-31 —— 初始化了 Redis 客户端,但 logic 层从未使用(全模块 grep 无 RedisCache 读取)
RedisCache *redis.RedisClient
func withRedisCache(srvKey string) { if config.Spec.Cache != "" { RedisCache = redis.New(...) } }
```
- **影响**:登录是最热路径且 `account` 无索引(以模型标签为准;线上是否另有手工索引无法在本工作区验证)→ 全表扫描随代理商规模线性劣化;`approve``created_at` 同样无索引标签,`Pending`/`Fetch``WHERE approve=0 ORDER BY created_at DESC` 会走排序(文件排序),审核台数据量上来后明显变慢;模块已接入 Redis 却零使用,问题 3 的限流/验证码也缺少现成存储。无 N+1`ref()` 为纯内存映射,未发现循环内 IO
- **建议**`account` 唯一索引(问题 7 一并解决)、`approve``created_at` 视查询计划补索引;登录结果/代理档案只读缓存(含失效策略,改密/审核/删除时失效);把 Redis 用于登录失败计数与验证码。
### P2
#### 11. 敏感信息与凭据进入日志/标准输出(含密码哈希、全部 SQL 与参数、含口令的 DSN
- **位置**`module/ec/market/internal/logic/agency/create.go:50,52``internal/models/impl.go:83``internal/impl/with.go:39``etc/market_dev.yaml:7`
- **证据**
```go
// create.go:50 —— 直接把含 Password 字段的整个模型打到 stdout
fmt.Println("mktModel = ", mktModel)
fmt.Println("err = ", err)
```
```go
// models/impl.go:83 —— postgres 分支无条件开启 Debug忽略 options.Debug三套 yaml 全部 Driver: postgres
db = db.Debug()
```
```go
// with.go:39 —— 启动日志打印整个 DBConfSource 为含 password=... 的 DSN
printer.Info("[BSM - %s] Databases: %v", vars.ServiceKey, config.Spec.Databases)
```
- **影响**bcrypt 哈希、代理商手机号/邮箱/证件 URL、全部查询与绑定参数含登录账号、改密语句会持续写入 stdout/日志文件DSN 口令进入启动日志。与问题 8 的"无审计日志"形成反差:该记的没记,不该记的全记了。日志级别/输出也无结构化字段(`log.Println``printer.Error` 混用,无 request_id/trace_id
- **建议**:删除 `fmt.Println` 调试语句;`NewPostgres` 尊重 `options.Debug`(并默认关闭);启动日志只打印 driver+host+dbname屏蔽 password统一结构化日志并接入 request_idSQL 参数脱敏(对手机号/证件/口令语句关闭参数打印)。
#### 12. 畸形 `Authorization` 头可触发 nil 解引用 panicmarket 侧无 recover/独立部署无外层鉴权
- **位置**`D:\work\bsm-sdk\core\crypto\token\jwt.go:64-72`(经 `service/meta.go:31` 被 market 各 logic 调用)、`module/ec/market/internal/logic/agency/get.go:20``internal/server/new.go:20-30`
- **证据**
```go
// SDK token/jwt.go:65-71 —— ParseWithClaims 对畸形 token 返回 (nil, err),随后直接取 token.Claims
token, err := jwt.ParseWithClaims(tokenstring, &Claims{}, func(token *jwt.Token) (any, error) { return []byte(t.SecretKey), nil })
if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } else { return nil, errcode.String(errcode.ErrTokenParse, err.Error()) }
```
```go
// 已核实 jwt v5.3.1 parser.go:140非三段式 token 返回 (nil, nil, err) → 上面 token.Claims 即空指针解引用
return nil, nil, newError("token contains an invalid number of segments", ErrTokenMalformed)
```
```go
// internal/server/new.go:23-30 —— 独立入口用裸 grpc.NewServer(),无拦截器、无 recover
if standalone { grpcServ = grpc.NewServer() }
```
- **影响**:在独立部署形态(`cmd/main` + `etc/market_*.yaml``MicroService.Enable:false`,无任何鉴权拦截器)下,任何请求只要带一个非 JWT 的 `Authorization` 值(如 `x`),就会在 `ParseMetaCtx` 内 panicgrpc-go 默认不 recover handler panic → 进程崩溃(远程 DoS。聚合部署中该 panic 被网关拦截器挡在前面,故当前不可达,但这是"靠外层挡"的脆弱依赖。(可达性判断:单机部署确定可达;聚合部署为条件不可达。)
- **建议**market 侧在 `ParseMetaCtx` 调用前做快速形状校验,或改为 `token.New(...).ParseJwt` 的安全封装(修 SDK`if err != nil || token == nil { return nil, ... }`,本模块可先用 helper 包一层);服务端注册 panic-recovery 拦截器;独立部署也必须装配 JWT 拦截器(不要依赖 `MicroService.Enable`)。
#### 13. `Updates(struct)` 零值不生效且不检查 `RowsAffected`:清空字段失败、对不存在的 identity 返回 "OK"
- **位置**`module/ec/market/internal/logic/agency/modify.go:28-55``internal/logic/supply/modify.go:38-48``agency/delete.go:26-36``agency/approve.go:29-37`
- **证据**
```go
// modify.go:28-44 —— mktModel 的复合字面量中没有 Identity 字段,因此下面 reply.Identity 必为空串(已逐字段核对)
mktModel := &models.MarketAgency{ Name: in.GetName(), Avatar: in.GetAvatar(), Account: in.GetAccount(), ..., Area: in.GetArea() }
// modify.go:45,50-55 —— 以结构体 Updates未检查 RowsAffectedreply.Identity 取的是入参模型(恒为空)
if err := impl.DBService.Where("identity=?", identity).Updates(&mktModel).Error; err != nil { ... }
return &pb.IdentityStatusReply{ Code: 0, Message: "OK", Identity: mktModel.Identity, Timeseq: time.Now().UnixMilli() }, nil
```
```go
// 已核实 gorm v1.31.2 callbacks/update.go:280,294 —— 结构体更新时按字段零值过滤if (ok || !isZero) && field.Updatable
value, isZero := field.ValueOf(stmt.Context, updatingValue)
if (ok || !isZero) && field.Updatable { set = append(set, clause.Assignment{...}) }
```
```go
// supply/modify.go:38-47 —— 同型delete.go:26 / approve.go:29 同样不看 RowsAffected
if err := impl.DBService.Where("identity=?", in.GetIdentity()).Updates(&mktModel).Error; err != nil {
```
- **影响**:① 无法把 `commission_rate`、备注、头像等字段改回 0/空串(业务上"取消佣金""清空备注"静默失败);② identity 不存在、已被软删除、或 `Status` 不满足时一律返回 `"OK"`,前端显示成功但零行更新;③ reply 的 `Identity` 恒为空字符串,客户端拿不到回执标识;④ 与问题 1 叠加时,攻击者还可以用"零行更新成功"探测 identity 是否存在(存在性 oracle
- **建议**:改用 `Updates(map[string]any{...})`(或 `Select("字段列表")`)显式表达要更新的列;检查 `RowsAffected`,为 0 时返回 `ErrNotFound`reply 回填真实 identity先用 identity 查回模型再更新);`Modify` 校验 `Status`/`Approve` 前置条件。
#### 14. 迁移与启动强耦合,且聚合部署路径从不迁移 market 表schema 漂移)
- **位置**`module/ec/market/internal/models/impl.go:15-18,39-43``service/dependencies.go:26-28``pkgs/ecmall/internal/service/market.go:10-19``D:\work\bsm-sdk\core\database\new.go:43-48,112-120``pkgs/ecmall/etc/default_dev.yaml:68-70`
- **证据**
```go
// models/impl.go:39-43 —— AutoMigrate 与建连写在同一个 New() 里,失败即 log.Fatalln
err = DBService.AutoMigrate(migrateTables...)
if err != nil { log.Fatalln(err); return err }
```
```go
// 但聚合路径只注入连接,不调用 models.Newpkgs/ecmall/internal/service/market.go:10-19 仅 Expose + Dependencies
func exposeMarket(srv *server.Server) error {
return moduleService.Expose(moduleService.ExposeOptions{ Dependencies: moduleService.Dependencies{ Redis: ..., DB: impl.DBService, ... }, ... })
}
```
```go
// 其他模块统一注册到 SDK 全局迁移表(如 module/ec/mall/internal/models/mall_store.go:31market 未注册
database.AppendMigrate(&MallStore{})
```
- **影响**`market_agency/market_supply` 只在独立进程启动时被迁移;`pkgs/ecmall`/`pkgs/all` 部署(`pkgs/ecmall/etc/default_dev.yaml:68-70` 明确"聚合从不执行模块的模型迁移与种子")下这两张表不会被创建/演进,缺表时表现为运行期 SQL 报错或静默失败,表结构随版本漂移且无迁移版本记录;迁移失败 `log.Fatalln` 直接终止进程(无回滚、无法只读启动)。
- **建议**:把 `database.AppendMigrate(&MarketAgency{})``&MarketSupply{}` 注册到 SDK 全局表,删除模块内私有 `migrateTables`;拆分为"连接/迁移/种子"三步独立命令(或交给 DDL 流水线),迁移失败返回错误而非 `Fatalln`;补一份基线 DDL 与索引说明,便于审计索引(问题 10与回滚。
#### 15. 配置缺陷:审核日志开关形同虚设、密钥配置被忽略而回退到 SDK 硬编码默认值、匿名白名单错误(另引用模板级网关缺陷)
- **位置**`module/ec/market/etc/market_dev.yaml:14,28``etc/market_prod.yaml:19``internal/config/config.go:15-23,38``D:\work\bsm-sdk\core\conf\types.go:7-16``D:\work\bsm-sdk\core\env\env.go:19``internal/server/new.go:26-30``cmd/main/main.go:30-33`
- **证据**
```yaml
# market_dev.yaml:14 —— SrvConfig 内嵌的 conf.Base 没有 OpLog 字段conf/types.go:7-16该键被 yaml 静默丢弃
OpLog: http://api-v2.traingo.cn/oplog/v2
# market_prod.yaml:19 —— 匿名白名单写的是 mall 的方法名dev 是 market.ping.helloprod/test 是 mall.ping.hello
MicroService: { Enable: false, Anonymous: [mall.ping.hello] }
```
```go
// config.go:38 —— 用 env.Runtime.JwtSecretKey而 yaml 的 SecretKey 从未赋值给它
encipher.New(env.Runtime.JwtSecretKey)
// SDK env.go:19 —— 未设置 BSM_JwtSecretKey 时回退到硬编码默认密钥
JwtSecretKey: GetEnvDefault("BSM_JwtSecretKey", "Cblocksmesh2022C"),
```
```go
// server/new.go:26-30 —— Mux 从未初始化nil而 cmd/main/main.go:32 把它当作网关处理器传入 → Gateway.Enable:true 但无路由
srv := &Server{ Ctx: context.Background(), Grpc: grpcServ, grpcConns: make(map[string]*grpc.ClientConn) }
```
- **影响**:① 业务操作日志(审核留痕,问题 8配置存在但代码不识别属"配置了以为有"的合规陷阱;② 独立部署使用 SDK 硬编码默认密钥 `Cblocksmesh2022C`16 字节恰好满足长度校验yaml 中的 `SecretKey: CHANGE_ME` 完全不生效,任何知道该默认值的人可伪造 token该密钥在 SDK 源码中可读);③ prod/test 匿名列表残留 `mall.ping.hello`,复制粘贴痕迹,且需要确认生产网关是否依赖该列表(同时 `Enable:false` 使注册到 etcd 的白名单从未生效);④ 独立进程 `Gateway.Enable:true` 实际是 `http.ListenAndServe(addr, nil)`HTTP 网关不可用——`internal/server/new.go` 只注册 gRPC`Mux` 恒为 nil从不调用 `gwRuntime.NewServeMux()`),而 HTTP handler 注册只在 `service/expose.go` 里做、`cmd/main` 从不调用 `Expose`;据主流程通知,这是**跨模块的模板级缺陷**(生成的 `pb.Register*HandlerServer` 首行即 `mux.Handle(...)`,无 nil 防御),本报告只作引用,不单独计入 market 的问题数。
- **建议**`SrvConfig` 增加 `OpLog` 字段并真正落地审核日志;启动时校验 `SecretKey` 非空且非默认值,显式 `env.NewEnv().JwtSecretKey = Spec.SecretKey`(与聚合一致),移除 SDK 默认密钥依赖;修正 prod/test 的匿名白名单并在 CI 校验 proto 方法名存在;`server.New` 初始化 `Mux` 或在 `Gateway.Enable` 时拒绝启动并打印明确错误。
#### 16. 健壮性与可观测性:`ctx` 未下传数据库、无超时/重试、无健康检查、优雅退出无超时
- **位置**`module/ec/market/internal/logic/agency/login.go:19-26`(其余 logic 同型)、`internal/logic/agency/fetch.go:22-25``cmd/main/main.go:37-40``internal/server/new.go:37-39`
- **证据**
```go
// 各处只把 ctx 用于 ParseMetaCtxDB 调用一律用全局 impl.DBService未 WithContext(ctx)
_, err = service.ParseMetaCtx(ctx, nil)
...
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
```
```go
// cmd/main/main.go:37 —— 唯一退出处理SDK service.Stop() 为无超时的 GracefulStopSDK service.go:142-144
defer srv.Stop()
```
- **影响**:客户端断开或网关超时不会取消 SQL慢查询/锁等待会持续占用连接(连接池耗尽风险);没有查询级超时/重试/熔断;除 gRPC reflection`internal/server/new.go:38`,且它本身是信息暴露面)外无 liveness/readiness优雅退出在长请求下可能无限等待无请求耗时/错误率指标,问题 6/7 的线上表现难以定位。
- **建议**:所有 DB 调用改 `impl.DBService.WithContext(ctx)`;在拦截器/中间件统一设置超时并暴露 metrics提供 `/healthz``/readyz``srv.Stop()` 加 context 超时并串接 SDK 的强制 `Stop`;生产关闭 reflection 或用配置控制。
#### 17. 可维护性agency 与 supply 大段复制、分层与依赖注入问题、死代码与魔术数字
- **位置**`internal/logic/supply/{create,fetch,get,modify,delete,ref}.go` 对比 `internal/logic/agency/*``internal/models/query.go:10-13``internal/server/new.go:17``service/dependencies.go:16,19-28``cmd/cli/main.go:5-7``internal/models/market_agency.go:18``internal/logic/supply/create.go:34`
- **证据**
```go
// supply/fetch.go:27-37 与 agency/fetch.go:27-43 除 model 与 status 过滤条件外逐行同构
if in.GetPageSize() < 10 { in.PageSize = 50 }
tx := impl.DBService.Model(&models.MarketSupply{}).Where("status = ?", 1)
if in.GetIdentity() != "" { tx.Where("identity = ?", in.GetIdentity()) }
if err := tx.Order("created_at desc").Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil {
```
```go
// supply/create.go:34,24 —— avatar 被注释丢弃proto 声明了该字段);只校验 nameaccount/password 可为空仍做 bcrypt
// Avatar: in.GetAvatar(),
if in.GetName() == "" { return nil, errcode.ErrInvalidArgument }
passwordHash, err := password.Hash(in.GetPassword())
```
```go
// models/query.go:5-7 / approve.go:29,34 —— 魔术数字approve 1/2 是"操作"2/-2 是"状态"
NormalStatus = 1
DisabledStatus = -1
```
- **影响**:两套子域约 6 个文件大段重复(含同一个分页缺陷一起复制,问题 5改一处漏一处`Supply` 缺 Login/审核/改密却复用"账号+密码"模型(密码无从使用、无审核却直接 `Status:1` 生效,见问题 8模型与业务能力不匹配`Salt`(残留的加盐列,改密时显式清空却无人读取)、`Server.grpcConns`(只初始化无使用)、`Dependencies.Cache``applyDependencies` 不处理)、`cmd/cli`(仅打印 Hello World`pb/blocks_compat.go` 手写兼容别名均属死代码/遗留;`approve` 语义在同一模块内一名两义;`service.ParseMetaCtx` 前多用注释掉的模板代码(`create.go:20-23``delete.go:17-21``supply/create.go:19-23`),掩盖"是否已鉴权"的判断。
- **建议**:抽出 `internal/logic/agency`/`supply` 共用的分页归一化、ref 映射、CRUD 基类(泛型或 shared helper统一状态常量与显式枚举类型`ApproveStatus``AccountStatus`)替换魔术数字;清理死代码与注释块;`Supply` 若不提供登录,模型应移除 password 或补全登录/审核能力并明确文档。
#### 18. `sdk/typescript` 与 proto 严重不一致(字段号冲突、方法缺失/改名、接口为空TypeScript 客户端不可用
- **位置**`sdk/typescript/agency.ts:466-494,582-645,655-680``sdk/typescript/blocks.ts:48-108``sdk/typescript/market/index.ts:155-172,200-209,250-259``sdk/typescript/market/const.ts:5-53`、对照 `proto/agency.proto:29-35,69-73``proto/const.proto:55-63`
- **证据**
```ts
// agency.ts:636-644 —— 暴露的是 Updateproto 中已改名 Modify且缺 Pending/Approve 两个方法
SetPassword: { path: "/market.Agency/SetPassword", ... responseSerialize: (message: dependency_1.market.StatusReply) ...
// agency.ts:483-494 —— SetPasswordRequest 字段号仍是 3/4且没有 identityproto 为 identity=1/old=2/new=3
get old_password() { return pb_1.Message.getFieldWithDefault(this, 3, "") as string; }
```
```ts
// blocks.ts:48-108 —— FetchRequest 仍是旧 mall 版本:字段 4 是 store_identity没有 identity/status/approveproto MarketFetchRequest: identity=4,keyword=5,status=6,approve=7
export class FetchRequest extends pb_1.Message { ... get store_identity() { ... getFieldWithDefault(this, 4, "") ...
```
```ts
// market/index.ts:155-172 —— generateAgencyClient 返回空对象Agency 接口无任何方法Data/Supply 同
export interface Agency { }
export function createAgencyClient(handler: RequestHandler): Agency { return { }; }
```
- **影响**TS 侧 `SetPassword` 发出去的字段号会被服务端解析为其它字段(`old_password` 落到 identity`MarketAgenctyItem` 字段号整体错位导致 Create/Modify/Get 请求语义全错;`Fetch``status/approve` 服务端收不到(过滤条件静默丢失,与问题 5 叠加HTTP SDK`market/index.ts`)根本不导出任何方法,前端无法调用;`const.ts` 里的 URL 常量则是最新的,进一步说明 SDK 生成物部分过期、无一致性校验。
- **建议**:以 proto 为准全量重新生成 TS含 grpc + HTTP 两套),在 CI 加"生成物与 proto 一致"的门禁(比较生成后 diff明确 SDK 版本的兼容策略;`const.ts` 与生成物放在同一次生成流程中。
#### 19. 测试覆盖严重不足,样例请求体自身与契约不符
- **位置**`internal/password/password_test.go:5-13`(全模块唯一单测)、`test/agency/create.http:5-14``test/agency/login.http:4-7``test/agency/approve.http:1``test/agency/pending.http:1``test/supply/fetch.http`
- **证据**
```markdown
test/agency/create.http —— 缺 password/phone 之外还缺必填 password服务端 create.go:24 会直接 ErrInvalidArgument
{ "account": "aaaa", "name": "aaaa", "org_name": "aa", "contact": "aaaaaa", "phone": "13246589952", "store_identity": "01970749-..." }
test/agency/approve.http:1 / pending.http:1 —— 指向生产域名
POST http://api.apinb.com/market.Agency/Approve
```
- **影响**:除 bcrypt 哈希/校验外无任何自动化测试;登录(含可枚举错误码)、鉴权/越权、审核状态机、分页边界(`Pending` 恒空、`Count` 错误、唯一性与并发重复注册、密码迁移兼容、SDK 与 proto 一致性均无用例;`.http` 样例缺必填字段(照抄会 400/报错),且含生产地址,存在误操作生产环境的风险。
- **建议**:补齐关键用例清单(见下)并纳入 CI① 登录成功/失败/禁用/未审核/不存在(断言错误码一致不可枚举);② 无 token、他人 token、过期 token、畸形 token 访问 Get/Modify/Delete/Approve/Fetch断言拒绝与无 panic③ 审核状态机矩阵0→2、0→-2、2→-2、-2→2、非法值与审批审计字段④ 分页page_no=0/-1/极大、page_size=0/5/极大)与 `count` 语义;⑤ 同账号并发 `Create` 的最终一致性;⑥ MD5 历史行登录/改密的兼容行为;⑦ `Modify` 清空字段与不存在 identity 的返回;⑧ SDK 生成物与 proto 的一致性。
### P3
#### 20. 响应字段与时间戳不一致
- **位置**`module/ec/market/internal/logic/supply/delete.go:34-38``internal/logic/agency/ref.go:16-35``internal/logic/agency/get.go:33-52`
- **证据**
```go
// supply/delete.go:37 —— 全模块其它地方用 UnixMilli此处为 UnixNano14 位 vs 19 位)
Timeseq: time.Now().UnixNano(),
```
```go
// ref.go 未回填 Approveget.go 未返回 CommissionRateref.go 有)—— 同类资源的字段集不一致
row := pb.MarketAgenctyItem{ Identity: in.Identity, ..., CommissionRate: in.CommissionRate, Status: int32(in.Status), ... }
```
- **影响**:前端按 `timeseq` 做时间比较/排序会得到 1970 年或越界值;列表页看不到审核状态(无法在列表筛选/展示审核结果,只能进详情);`Get``commission_rate` 导致同一实体在不同接口返回不同字段集。
- **建议**:统一时间戳单位(建议 `UnixMilli`)并抽公共构造函数;`ref()` 回填 `Approve``Get` 补齐 `CommissionRate`/`CreatedAt` 等字段并写接口字段一致性用例。
#### 21. proto 语义冲突、命名拼写与注释不一致
- **位置**`module/ec/market/proto/agency.proto:44,65,80-83``internal/models/market_supply.go:25``market_agency.go:30`
- **证据**
```proto
message MarketAgenctyItem { // 拼写错误Agencty已扩散到 Go/TS 全链路
int32 approve = 20;// 审核状态0待审核 2审核通过 -2审核未通过
message ApproveRequest { int32 approve = 1; // 1:通过 2:拒绝
```
```go
// market_supply.go:25 的注释含 "1为审核中"market_agency.go:30 的同字段注释没有该状态,两表语义已分叉
Approve int8 `gorm:"column:approve;default:0;" json:"approve"` // 状态:-2,认证未通过0为未认证2为认证成功
```
- **影响**`approve` 在请求/实体两名两义是典型的错用来源(实际代码 approve.go:29,34 已按"1=通过、2=拒绝"映射为 2/-2若客户端直接复用实体枚举会反向操作审核结果Agency 与 Supply 的审批状态集合不一致Supply 的"1 审核中"无人写入。
- **建议**:定义独立枚举消息(`enum ApproveAction { PASS = 1; REJECT = 2; }` / `enum AuditStatus { PENDING=0; APPROVED=2; REJECTED=-2; }`)并加 `_UNSPECIFIED = 0` 守卫;统一两表的审核状态机;修正 `MarketAgenctyItem` 拼写(需 proto/Go/TS 同步重生成,做好兼容评估)。
#### 22. 文档与实现不一致
- **位置**`module/ec/market/README.md:1-3``wiki/audit-2026-08-10.md:27`、根 `README.md:156-162``test/agency/create.http:12-13`
- **证据**
```markdown
module/ec/market/README.md全文 3 行)
# market
market agency
```
```markdown
wiki/audit-2026-08-10.md:27 —— 声称历史 MD5 记录"需要通过密码重置流程迁移",但 market 无任何重置 RPC见问题 6
```
- **影响**:模块 README 无接口、权限、状态机、迁移、部署说明(信息实际散落在 wiki 与代码里,且 wiki 的"已完成迁移"结论会误导后续维护);`create.http` 残留 `contact`/`store_identity` 等不属于本模块的字段,无法作为联调样例使用。
- **建议**README 补:服务/方法清单、鉴权与角色要求、审核状态机、分页约定、密码哈希与迁移策略、部署形态(独立 vs 聚合)与建表方式;修订 wiki 结论;清理样例请求。
#### 23. 部署与运维细节
- **位置**`module/ec/market/etc/supervisor.bsm-ec-market.conf:6``internal/impl/impl.go:9-11``internal/impl/with.go:50-87`
- **证据**
```
user=root # supervisor 以 root 运行
stdout_logfile=/data/app/logs/bsm-ec-market.log # 未见轮转配置项
```
```go
func NewImpl() { withRedisCache(vars.ServiceKey); withDatabases(); withEtcd() } // 未启用则 panic/退出
```
- **影响**:以 root 运行放大被利用后的影响面;日志文件无显式轮转(依赖 supervisor 默认值),配合问题 11 的 SQL 全量日志易打满磁盘Etcd 配置缺失即 panic`with.go:53`)使独立部署无法在无 etcd 环境下启动;`impl.RedisCache` 初始化后未使用(问题 10却让启动依赖 Redis 可用。
- **建议**`user=`非 root 专用账号;配置 `stdout_logfile_maxbytes/backups`Etcd/Redis 改为按需可选(`Etcd == nil` 时跳过,不 panic把 Redis 真正用于限流/缓存,否则移除启动依赖。
#### 24. 其余一致性与清理项
- **位置**`module/ec/market/internal/logic/supply/create.go:34``internal/logic/agency/create.go:20-23``internal/logic/agency/delete.go:17-21``internal/logic/agency/ref.go:13-37``internal/server/new.go:17`
- **证据**
```go
// supply/create.go:34 —— proto 声明的 avatar 被注释掉,前端设置头像无效
// Avatar: in.GetAvatar(),
// agency/create.go:20-23 —— 注释掉的鉴权模板代码需人工判断是否已鉴权delete.go:17-21、supply/create.go:19-23 同)
// _, err = service.ParseMetaCtx(ctx, nil)
// if err != nil { return nil, err }
```
```go
grpcConns map[string]*grpc.ClientConn // 只初始化从未使用new.go:29
```
- **影响**`Supply.Avatar` 静默丢失;注释掉的鉴权模板让"当前是否受保护"需要读网关配置才能判断(问题 1 的直接诱因),且极易被误删/误留;`ref()``list` 命名返回与 `row` 取址Go 1.22+ 每轮迭代独立变量,已核实无别名问题,`go.mod:3` 为 go 1.26.5)虽正确但易被误读为经典循环取址 bug`grpcConns` 等死字段增加阅读成本。
- **建议**:补齐 Supply avatar 映射或从 proto 移除;删除注释掉的鉴权模板,改成"显式鉴权 + 注释说明权限要求";清理未使用字段与命名返回值。
## 4. 推荐优化方案
1. **先修授权P0-1**:在 market 内建立最小权限模型——`ParseMetaCtx(ctx, nil)` 改为带角色/作用域的显式校验;自助接口一律以 `claims.Identity` 为准管理接口Approve/Delete/Fetch/Pending/管理向 Modify只允许管理员角色聚合层匿名白名单保持只有 `Login`,并为 market 管理方法加角色白名单。补一组"跨身份访问必被拒绝"的回归用例。
2. **统一 token 实现P0-2**`agency.Login` 改用 `token.New(env.Runtime.JwtSecretKey).GenerateJwt`,废止 `encipher.GenerateTokenAes` 的 AES 路径(或将其改名 `EncryptClaimsAes` 避免误用),保证"签发=校验"同源;上线前跑通"登录→带 token 调 Get"端到端用例,并把 `env.Runtime.JwtSecretKey` 从配置显式注入、禁止使用 SDK 默认密钥(问题 15
3. **认证面加固P1-3/12**:统一"账号或密码错误"响应;用已接入的 Redis 做失败计数/IP 限流与验证码(真正校验 `verify`);注册 panic-recovery 拦截器并给独立部署装配 JWT 拦截器;登录/改密/审核全部落审计日志(同时修好 `OpLog` 配置,问题 15
4. **契约与数据一致性P1-5/7/20/21**:抽取统一分页(`pageSize` 上限、`count` 语义、`keyword/params` 白名单);`account` 唯一索引 + 幂等注册 + 冲突错误码;审核状态机枚举化并落审批审计;列表裁剪敏感字段;补索引(`account``approve``created_at`)。
5. **密码与迁移P1-6**:统计历史 MD5 行,一次性兼容登录(成功后重写为 bcrypt或提供受控重置流程完成后移除 `Salt` 列与兼容分支;同步修订 wiki。
6. **可观测与健壮性P2-11/13/14/16/19**DB 调用带 `ctx` 与超时;`Updates(map)` + `RowsAffected` 校验;迁移拆出启动并注册到 `database.AppendMigrate`;结构化日志 + 脱敏 + 健康检查 + 优雅退出超时;补齐第 19 条列出的测试矩阵并把 SDK/proto 一致性纳入 CI。
**上线前建议门禁(按依赖顺序)**`P0-1 → P0-2 → P1-3/4 → P1-5/7 → P1-6/8 → P2-11/13`
## 5. TODO 清单
- [ ] **P0-1** 为 market 管理接口加角色与归属校验,自助接口只允许操作 `claims.Identity`|验收:普通会员 token 调 `Agency/Approve``Agency/Delete`、他人 `Agency/Modify|Get` 全部被拒(新增用例覆盖),管理员路径放行|涉及:`module/ec/market/internal/logic/agency/approve.go:15``delete.go:16``modify.go:20``get.go:20``fetch.go:22``pkgs/ecmall/internal/server/authorization.go:42`
- [ ] **P0-2** 代理商登录改用与 `ParseMetaCtx`/网关一致的 HS256 JWT 签发|验收:登录返回的 token 通过聚合网关与 `service.ParseMetaCtx``登录 → Agency/Get` 端到端用例通过;模块内不再调用 AES 版 `GenerateTokenAes`|涉及:`module/ec/market/internal/logic/agency/login.go:53`
- [ ] **P1-3** 登录统一错误语义 + 失败限流 + 真实校验 `verify` 验证码验收错误码不可区分账号是否存在N 次失败后拒绝并记录审计;`verify` 错误时登录失败|涉及:`login.go:26-41``internal/impl/with.go:19`
- [ ] **P1-4** 移除自助接口的请求体 identity 覆盖,`SetPassword` 校验归属并增加新密码强度要求|验收:传入他人 identity 无法读取/修改数据;弱密码被拒|涉及:`get.go:25``modify.go:25``set_password.go:24-30`
- [ ] **P1-5** 修分页与过滤:`Count``cnt``Pending``pageSize` 守卫改 `<1``pageSize` 加上限、实现 `keyword/params`、列表裁剪证件字段|验收:默认参数下 `Pending` 有数据;`count` 为总数;`page_size=10^6` 被拒或被截断;列表响应不含 `id_before/id_after`|涉及:`agency/fetch.go:43-51``agency/pending.go:31-38``supply/fetch.go:30-43``agency/ref.go:16`
- [ ] **P1-6** 建立 MD5 历史账号迁移/重置路径并统计存量|验收:存量行数有报表;兼容登录成功后该行被重写为 bcrypt含审计无重置 RPC 的问题关闭或明确保留策略|涉及:`internal/password/password.go:10``agency/set_password.go:37``wiki/audit-2026-08-10.md:27`
- [ ] **P1-7**`account` 建唯一索引并给注册加查重/幂等/限流|验收:并发注册同一 account 只有一行成功,另一次返回明确冲突错误|涉及:`internal/models/market_agency.go:15``agency/create.go:24-55``models/impl.go:39`
- [ ] **P1-8** 审核状态机显式化 + 审批审计 + 变更证件回滚审核态 + 启用/禁用接口|验收:非法 approve 值返回错误;审批记录含审批人/时间/意见;`Modify` 改证件后 approve 归 0可置 `status=-1` 并被登录拒绝|涉及:`agency/approve.go:27-44``agency/modify.go:28-45``agency/create.go:32``models/query.go:5`
- [ ] **P1-9** `Data` 服务占位接口返回 `Unimplemented`或下架服务验收5 个方法均返回明确未实现错误不再返回成功空响应proto/wiki 标注一致|涉及:`internal/logic/data/overview.go:18``member_fetch.go:26``member_details.go:24``order_fetch.go:26``order_details.go:24`
- [ ] **P1-10** 热路径索引与缓存|验收:`account` 查询走索引EXPLAIN 验证)、`WHERE approve=0 ORDER BY created_at DESC` 无文件排序;登录/档案读有缓存与失效策略|涉及:`internal/models/market_agency.go:15``agency/login.go:26``internal/impl/with.go:19`
- [ ] **P2-11** 日志脱敏与清理调试输出|验收:日志中不出现 bcrypt 哈希/手机号/证件 URL/DSN 口令;`NewPostgres` 遵循 Debug 配置|涉及:`agency/create.go:50-52``internal/models/impl.go:83``internal/impl/with.go:39`
- [ ] **P2-12** 消除畸形 token 导致的 panic 并补 recover 拦截器|验收:`Authorization: x` 请求返回鉴权错误且进程存活(新增用例)|涉及:`D:\work\bsm-sdk\core\crypto\token\jwt.go:64``internal/server/new.go:20-30`
- [ ] **P2-13** 更新语句改 `map`/`Select` 并校验 `RowsAffected`reply 回填真实 identity验收可将佣金/备注清为 0/空identity 不存在时返回 NotFound涉及`agency/modify.go:45-55``supply/modify.go:38-47``agency/delete.go:26`
- [ ] **P2-14** 迁移与启动解耦并接入全局迁移表|验收:`database.AppendMigrate(&MarketAgency{}, &MarketSupply{})` 生效,聚合部署可建表;迁移失败返回错误而非退出进程|涉及:`internal/models/impl.go:15-43``pkgs/ecmall/internal/service/market.go:10`
- [ ] **P2-15** 配置修复:`OpLog` 字段、密钥注入、匿名白名单(独立入口 `Mux` 为 nil 的网关缺陷属模板级,随全局修复)|验收:审核日志落地;启动拒绝默认/空密钥prod/test 白名单为 market 方法|涉及:`internal/config/config.go:15-38``internal/server/new.go:26-30``cmd/main/main.go:32``etc/market_prod.yaml:19`
- [ ] **P2-16** `ctx` 下传 DB + 超时/健康检查/优雅退出验收DB 调用使用 `WithContext`;客户端断开后查询被取消;提供健康端点;关停有超时上限|涉及:`agency/login.go:26`(及全部 logic`cmd/main/main.go:37`
- [ ] **P2-18** 重新生成并校验 TypeScript SDK验收字段号/方法名与 proto 完全一致(`Modify``Pending``Approve` 齐全);`createAgencyClient` 暴露全部方法CI 生成物一致性检查通过|涉及:`sdk/typescript/agency.ts:466-680``blocks.ts:48``market/index.ts:155-259`
- [ ] **P2-19** 建立测试矩阵并接入 CI验收第 19 条 8 类用例全部落地且通过;`.http` 样例改用必填字段与本地地址|涉及:`internal/password/password_test.go:5``test/agency/*.http`
- [ ] **P3-20~24** 一致性与清理:时间戳统一、`ref/Get` 字段对齐、审核枚举与拼写、README/wiki 更新、supervisor 降权与日志轮转、死代码与注释块清理|验收:同一实体在各接口字段集一致;`timeseq` 单位统一README 含接口/权限/迁移/部署说明;无注释掉的鉴权模板与未使用字段|涉及:`supply/delete.go:37``agency/ref.go:16``agency/get.go:33``proto/agency.proto:44``README.md:1``etc/supervisor.bsm-ec-market.conf:6`
## 6. 审计摘要(供汇总使用)
- 问题数P0=2 P1=8 P2=9 P3=5合计 24
- 最高风险一句话market 全部管理接口(审核/删除/修改/列表)只校验"JWT 签名有效"而不校验角色与归属,任何已登录的普通会员 token 就能自审通过、删除他人代理商、篡改佣金比例并拉取全量身份证资料;同时代理商登录签发的 token 根本不是 JWT导致登录后系统不可用、可用 token 只剩跨域签发者。
- 最优先 3 个动作1) 为 `Agency/Approve|Delete|Modify|Fetch|Pending` 加角色/归属校验、自助接口只认 `claims.Identity`P0-12) `agency.Login` 改用与 `ParseMetaCtx`/聚合网关一致的 HS256 JWT 签发并加端到端回归P0-23) 登录防爆破+验证码+统一错误语义,并把 `Data` 占位接口改为 `Unimplemented`、修分页/`Pending` 恒空P1-3/5/9
- 未能覆盖/无法验证的部分:真实数据库 schema 与索引、线上是否仍存在 MD5 历史行、生产配置实际取值(`SecretKey`/`OpLog`/匿名白名单是否被覆盖)、`pkgs/all` 的实际启用服务集合、运行时行为(未启动服务、未跑集成测试)、`pb/` 下 11 个生成文件未逐行阅读(仅核对类型与字段号)、`sdk/typescript/{blocks,data}.ts` 为抽样阅读。结论中凡涉及"未实现/可达性/线上存量"的推断均已在对应条目标注条件。