# 审计报告: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 KB,100% 覆盖)** - 入口与装配:`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 不是 JWT(AES-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 —— 用 encipher(AES)生成 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 // 而校验方要求 JWT:SDK 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`,即**登录接口事实上不可用**,登录后的任何调用都失败;② 反过来,能通过校验的只有其他域签发的 JWT(passport 会员/mgt 管理员),使问题 1 的利用门槛降到"任意注册会员";③ 两处"GenerateTokenAes"同名不同实现(passport 的是 JWT,market/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-29(modify.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 —— 守卫写成 <0,proto 默认 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 个方法)中不存在"忘记密码/重置密码"RPC,wiki 声称的"密码重置流程迁移"在 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 恒为 1;models/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` 硬编码 1,proto 无 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 —— 具名返回值 + 裸 return:reply 与 err 都保持零值(nil, nil),RPC 表现为"成功但空" 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 —— 启动日志打印整个 DBConf(Source 为含 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_id;SQL 参数脱敏(对手机号/证件/口令语句关闭参数打印)。 #### 12. 畸形 `Authorization` 头可触发 nil 解引用 panic,market 侧无 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` 内 panic;grpc-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;未检查 RowsAffected,reply.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.New(pkgs/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:31);market 未注册 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.hello,prod/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 用于 ParseMetaCtx,DB 调用一律用全局 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() 为无超时的 GracefulStop(SDK 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 声明了该字段);只校验 name,account/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 —— 暴露的是 Update(proto 中已改名 Modify),且缺 Pending/Approve 两个方法 SetPassword: { path: "/market.Agency/SetPassword", ... responseSerialize: (message: dependency_1.market.StatusReply) ... // agency.ts:483-494 —— SetPasswordRequest 字段号仍是 3/4,且没有 identity(proto 为 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/approve(proto 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,此处为 UnixNano(14 位 vs 19 位) Timeseq: time.Now().UnixNano(), ``` ```go // ref.go 未回填 Approve;get.go 未返回 CommissionRate(ref.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-1);2) `agency.Login` 改用与 `ParseMetaCtx`/聚合网关一致的 HS256 JWT 签发并加端到端回归(P0-2);3) 登录防爆破+验证码+统一错误语义,并把 `Data` 占位接口改为 `Unimplemented`、修分页/`Pending` 恒空(P1-3/5/9)。 - 未能覆盖/无法验证的部分:真实数据库 schema 与索引、线上是否仍存在 MD5 历史行、生产配置实际取值(`SecretKey`/`OpLog`/匿名白名单是否被覆盖)、`pkgs/all` 的实际启用服务集合、运行时行为(未启动服务、未跑集成测试)、`pb/` 下 11 个生成文件未逐行阅读(仅核对类型与字段号)、`sdk/typescript/{blocks,data}.ts` 为抽样阅读。结论中凡涉及"未实现/可达性/线上存量"的推断均已在对应条目标注条件。