Files
full/docs/feedback.md

198 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-09-22 18:53:53 +08:00
# feedback意见反馈管理代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/feedback` |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件 19 个、821 行(含 `test/rpc/rpc.go` 23 行空壳);`proto` 2 个(`feedback.proto``const.proto``pb/` 生成文件 4 个 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、聚合入口 `pkgs/all``pkgs/all/internal/service/feedback.go` |
| 对外协议 | gRPC + REST Gateway前缀 `/feedback.Method` |
| 结论摘要 | 越权缺陷成堆:`Get`/`Delete`/`Remark` 连身份都不解析,`Modify` 解析了身份却仍按调用方传入的 `identity` 更新,四个接口**均无归属校验**IDOR`Modify` 会把记录 `identity` 改写成随机 UUID 并把归属改成攻击者;`Modify` 处理附件时未生成 `identity`,子表 `identity` 是唯一索引,附件 ≥2 条必然唯一键冲突;`List`/`Get``Preload("Images")`,附件永远为空;此外 `etc/` 下 4 份 yaml 全部不符合 SDK 配置约定(`Name`/`ListenOn`/`Dsn` + 顶层 `Anonymous`),独立进程启动即 `log.Fatalln`。 |
## 1. 服务定位与职责
用户意见/反馈的提交与后台管理:提交反馈(含图片、附件)、按条件分页查询、查看详情、修改、删除、填写备注并流转状态。数据模型为"反馈主表 + 图片子表 + 附件子表"。它不负责附件文件本身的上传(那是 `module/base/fts` 的职责),只保存 URL/路径。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New("Feedback")``impl.NewImpl``server.New(nil)``service.New(...).Start()` |
| `cmd/cli/main.go` | 命令行入口,仅 `log.Println("Hello World!")`7 行,占位) |
| `internal/config/config.go` | 配置结构(`Base`/`Databases`/`MicroService`/`Rpc`/`Gateway`/`APM`/`Etcd`)与 `conf.NotNil` 校验 |
| `internal/impl/impl.go` | 初始化 DB / Redis / Etcd / 内存缓存 |
| `internal/logic/method/list.go` | `List`:分页 + 按 identity/agency/username/status/category 过滤 |
| `internal/logic/method/get.go` | `Get`:按 `identity` 查详情 |
| `internal/logic/method/add.go` | `Add`:创建主表 + 图片 + 附件 |
| `internal/logic/method/modify.go` | `Modify`:删子表 → 更新主表 → 重建子表 |
| `internal/logic/method/delete.go` | `Delete`:按 `identity` 删主表 |
| `internal/logic/method/remark.go` | `Remark`:更新 `remark` + `status` |
| `internal/logic/method/ref.go` | `convert`:模型 → pb 转换(含图片、附件) |
| `internal/models/feedback_item.go` | `feedback_item` 主表模型 + 迁移注册 |
| `internal/models/feedback_images.go` | `feedback_images` 图片子表 |
| `internal/models/feedback_accessory.go` | `feedback_accessory` 附件子表 |
| `internal/server/{new,method_server}.go` | 注册 `Method` 到 gRPChandler 转发 logic |
| `internal/routers` | **不存在**(走 gRPC + HTTP Gateway |
| `service/{expose,dependencies}.go` | 聚合宿主注入接口 |
| `etc/{feedback.yaml,feedback_dev,feedback_prod,feedback_test}.yaml` | 4 份配置,字段命名与 SDK 约定不符(见 6.3 |
| `test/add.http``test/rpc/rpc.go` | 手工用例JSON 非法)+ 注释空壳 |
## 3. 接口清单
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/feedback.Method/List` | 分页查询反馈(条件过滤) | 网关 JWT + `service.ParseMetaCtx``list.go:19` | `internal/logic/method/list.go` |
| POST | `/feedback.Method/Get` | 按 `identity` 查详情(返回 email/phone | 网关 JWT**进程内不解析身份**`get.go` 无 ParseMetaCtx | `internal/logic/method/get.go` |
| POST | `/feedback.Method/Add` | 新增反馈 | 网关 JWT + `ParseMetaCtx``add.go:21` | `internal/logic/method/add.go` |
| POST | `/feedback.Method/Modify` | 按 `identity` 修改 | 网关 JWT + `ParseMetaCtx``modify.go:24` | `internal/logic/method/modify.go` |
| POST | `/feedback.Method/Delete` | 按 `identity` 删除 | 网关 JWT**进程内不解析身份**`delete.go` | `internal/logic/method/delete.go` |
| POST | `/feedback.Method/Remark` | 按 `identity` 更新备注 + 状态 | 网关 JWT**进程内不解析身份**`remark.go` | `internal/logic/method/remark.go` |
**声明但未实现的接口**:无。`proto/feedback.proto:6-13``Method` 服务 6 个 rpc 与 `pb/feedback_grpc.pb.go``MethodServer``internal/server/method_server.go:18-40` 的转发方法一一对应。
> 鉴权补充:
> 1. 模块 yaml 里列出的 `feedback.Check.Hello/Updates`、`feedback.Data.Configure/Areas/Tags``etc/feedback_dev.yaml:19-23`)在本模块 proto 中**根本不存在**,属死配置,详见 6.3。
> 2. 聚合入口 `pkgs/all/etc/default_dev.yaml:24-43` 的 `Authorization.Anonymous` 也未包含 `/feedback.Method/*`,故上线形态下 6 个接口都要求携带有效 JWT——但"有效 JWT"仅代表"是某个登录用户",接口内部不做任何归属判断,见 6.1。
## 4. 数据模型与表
### 表 `feedback_item``internal/models/feedback_item.go`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 来自 `types.Std_IICUDS` |
| `identity` | varchar(36) | uniqueIndex | 记录唯一标识(`D:\work\bsm-sdk\core\types\db.go:30` |
| `created_at`/`updated_at`/`deleted_at` | timestamp | - | 带软删 |
| `passport_id` | uint | Index | 提交人 ID`types.Std_Passport` |
| `passport_identity` | varchar(36) | Index | 提交人标识,`List` 的归属过滤字段 |
| `status` | int32 | default 1 | 1 未处理 / 2 已处理(**与内嵌 `Std_IICUDS.Status` int8 同列冲突**,见 6.2 |
| `category` / `user_name` / `email` / `phone` | varchar | - | 分类 / 用户名 / 邮箱 / 手机(**含 PII** |
| `title`255/ `content`500/ `remark`500 | varchar | - | 标题 / 内容 / 备注 |
| `agency` | varchar(255) | - | 机构 |
| `images` / `accessories` | - | `foreignKey:ItemIdentity` | GORM 关联字段(非列) |
### 表 `feedback_images` / `feedback_accessory`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `identity` | varchar(36) | uniqueIndex | 子表唯一标识(来自 `Std_IICUDS` |
| `item_identity` | varchar(36) | default '' | 关联主表 `identity`,无 DB 级外键 |
| `url`(图片) | varchar(255) | default '' | 图片地址 |
| `title` / `file_path`(附件) | varchar(255) / varchar(500) | not null | 附件标题 / 附件路径 |
## 5. 核心流程
```mermaid
flowchart TD
A["POST /feedback.Method/Modify"] --> B["service.ParseMetaCtx 解析 auth"]
B --> C["校验 in.Identity 非空"]
C --> D["构造 recordIdentity = utils.UUID(),归属改写为 auth"]
D --> E["按 item_identity 删除 feedback_images / feedback_accessory"]
E --> F["Where(identity = in.Identity).Updates(record)"]
F --> G["GORM 顺带 upsert 非空 HasMany 关联"]
G --> H["显式 Create(&record.Images) / Create(&record.Accessories)"]
I["POST /feedback.Method/Get"] --> J["Where(identity = in.Identity).First无身份、无归属校验"]
J --> K["返回含 email / phone 的完整记录"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/method/get.go:19-41`(关键:`:21` 只校验 `identity` 非空、`:29``identity` 查询) | `Get` **完全不解析调用方身份**,仅按请求体里的 `identity` 取记录,且返回体包含 `email`/`phone``ref.go:17-18`)。任何持有效 JWT 的用户遍历 `identity` 即可读取**全部用户**的反馈与联系方式IDOR + PII 泄露)。 |
| **高** | `internal/logic/method/delete.go:19-29`(关键:`:26` | `Delete` **不解析身份、不做归属校验**`Where("identity = ?", in.GetIdentity()).Delete(...)` 直接删除。任何登录用户可删除他人反馈(软删,`deleted_at`)。 |
| **高** | `internal/logic/method/remark.go:19-35`(关键:`:21`,`:28`,`:32` | `Remark` **不解析身份、不做归属校验**,且 `status` 取自请求体(`remark.go:28`)→ 任何登录用户可改写任意反馈的备注与处理状态(可把"已处理"回退或伪造)。 |
| **高** | `internal/logic/method/modify.go:22-27,30,89` | `Modify` 虽然解析了 `auth`,但**只用它覆写归属字段**`modify.go:39-42`),更新条件仍是调用方传入的 `in.Identity``modify.go:89`)→ 可篡改他人反馈;更严重的是会把该记录的 `passport_identity` 改成攻击者自己,**完成"夺权"**:原主人因 `List``passport_identity` 过滤(`list.go:43`)而再也看不到自己的反馈。 |
| 中 | `internal/logic/method/list.go:38-45` | `List` 的过滤是"二选一":传入 `agency` 时**只**按 `agency` 过滤,`passport_identity` 条件被跳过 → 任意登录用户只要知道机构名,就能枚举该机构下**全部**反馈,且响应含 `email`/`phone``ref.go:17-18`)。 |
| 中 | `internal/logic/method/list.go:41-44` | 当 `auth.Identity` 为空串时,`passport_identity` 过滤被跳过(`if userIdentity != ""`),查询退化为无归属条件的分页查询。`service.ParseMetaCtx` 只校验 JWT 签名与有效期、不校验 `identity` 非空(`D:\work\bsm-sdk\core\service\meta.go:19-47`),因此该分支在 token 未携带 `identity` 时可达。 |
| 低 | `internal/logic/method/ref.go:13-26` | 列表/详情统一输出 `email``phone`PII 在所有读接口上无脱敏、无权限分级。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/method/modify.go:35-38,89` | `Updates(record)``record``Std_IICUDS.Identity = utils.UUID()`(新 UUID。GORM 结构体 `Updates` 只忽略零值,`Identity` 非零 → **主表 `identity` 列被改写成新 UUID**。由于 `Where` 用的是旧 `identity`,更新后原 `identity` 立即失效,`Get`/`Modify`/`Delete`/`Remark` 全部找不到该记录,任何外部引用(如工单、通知)随之断裂。 |
| **高** | `internal/logic/method/modify.go:70-76` | 构造附件时**未设置 `Identity`**(对比 `add.go:57-66` 明确赋 `utils.UUID()``FeedbackAccessory``identity` 取零值 `""`;而子表 `identity``uniqueIndex``D:\work\bsm-sdk\core\types\db.go:30`)→ **附件 ≥2 条时必然唯一键冲突**;即使只传 1 条,也会写入一行 `identity=""` 的脏数据,后续再改就再也插不进去。 |
| **高** | `internal/logic/method/modify.go:50-52,89,95-106` | 关联被写入两遍。GORM v2 的 Update 处理器注册了 `gorm:save_after_associations``gorm@v1.31.2/callbacks/callbacks.go:72``SaveAfterAssociations(false)` 对 ReflectValue 为 Struct 的 `HasMany` 字段会调用 `saveAssociations``callbacks/associations.go:190-236`),最终执行 `tx.Create(values)``callbacks/associations.go:431`,非 `FullSaveAssociations` 时带 `ON CONFLICT DO NOTHING`,见 `:371-372`)。也就是说 `modify.go:89``Updates` 已把 `Images`/`Accessories` 插入库中,随后 `modify.go:96`/`:102` 又对同一批(同 `identity`)显式 `Create` → 第二次插入撞唯一索引,接口返回错误,而主表与子表**其实已经改了一半**(无事务包裹)。 |
| 中 | `internal/logic/method/delete.go:25-26` | 注释写"GORM会自动处理关联的图片和附件删除",但模型只在关联字段上声明了 `foreignKey:ItemIdentity``feedback_item.go:23-24`**没有 `OnDelete:CASCADE`**,子表也无 DB 级外键 → `Delete` 只删主表,`feedback_images`/`feedback_accessory` 成为孤儿数据(`Modify` 是手动先删子表的,`Delete` 没做)。 |
| 中 | `internal/models/feedback_item.go:12``:18` | `status` 列被定义两次:内嵌 `types.Std_IICUDS.Status`int8`default:0;index`)与自定义 `Status int32``default:1`。GORM 按"最短绑定路径优先"解析(`gorm@v1.31.2/schema/schema.go:238`),外层字段胜出、内嵌字段被遮蔽 → 实际建表丢失 `index` 标签,默认值语义也从 SDK 约定的 0 变成 1。 |
| 中 | `internal/logic/method/list.go:35``internal/logic/method/get.go:29` | 两处只 `Preload("Images")`**没有 `Preload("Accessories")`**,而 `ref.go:38-45` 会转换并输出附件 → `List`/`Get` 返回的 `accessories` 恒为空数组,与 `proto/feedback.proto:46``repeated FeedbackAccessory accessories = 12`)的契约不符。 |
| 低 | `internal/logic/method/list.go:25-32,71` | `page` 无上限钳制(只保证 `>= 1``offset = (page-1)*size` 直接下推为 `OFFSET`,传超大 `page` 会造成大偏移慢查询。 |
| 低 | `internal/logic/method/remark.go:37-40``internal/logic/method/delete.go:31-34` | 返回 `pb.StatusReply` 只填 `Message`/`Timeseq``code` 恒为 0`proto/feedback.proto:126`),调用方无法从 `code` 判断成功/失败。 |
| — | `internal/logic/method/list.go:71` | `.Find(&list).Count(&count)` 复用同一 `Statement` 看似可疑,但 GORM 在 `Execute` 结束时会重置 SQL`gorm@v1.31.2/callbacks.go:149-152``Count` 重建为 `SELECT count(*) ... LIMIT n OFFSET m`,聚合结果单行不受 `LIMIT` 影响,`count` 等于命中总数。**非缺陷**(特此说明)。 |
| — | `internal/logic/method/add.go:36` | `Add` 接受调用方传入的 `status`,与 `proto/feedback.proto:74` 注释"也可以调用方自行设置"一致,属设计约定;但结合 6.1 的 `Remark` 问题,状态流转整体缺少权限约束。 |
### 6.3 未完成实现
- **独立进程无法启动4 份 etc 配置全不符合 SDK 约定**。`etc/feedback_dev.yaml:1-4` 使用 `Name:` / `ListenOn:` / `Dsn:`,而 SDK 的 `conf.Base` yaml 标签是 `Service`/`Port`/`Cache`/`SecretKey`/`BindIP``D:\work\bsm-sdk\core\conf\types.go:7-16`);更关键的是 `conf.New` 在解析前会校验文件文本必须包含 `Service:`,否则 `log.Fatalln("ERROR: Service Not Nil")``D:\work\bsm-sdk\core\conf\new.go:58-60`)。`feedback.yaml``feedback_dev.yaml``feedback_prod.yaml``feedback_test.yaml` 都只有 `ServiceName:`(不匹配 `Service:` 子串),默认 `BSM_RuntimeMode=dev``D:\work\bsm-sdk\core\env\env.go:20`)会加载 `etc/feedback_dev.yaml`**进程直接 Fatal 退出**。即便绕过该校验,因无 `Databases:` 段,`with.Databases` 也会 `panic("No Database Source Found !")``D:\work\bsm-sdk\core\with\databases.go:13-15`)。`etc/feedback_prod.yaml:4``Dsn: postgres://prod:***@192.168.0.224:5432/scf...`,即"生产配置"同样不可用。
- **匿名清单整段失效且内容不存在**`etc/feedback_dev.yaml:16-23` 用的是顶层 `Anonymous:` + `Key:`/`Urls:` 结构,而 SDK 期望 `MicroService: {Enable: bool, Anonymous: []string}``conf\types.go:23-26``internal/config/config.go:17-25``SrvConfig` 也没有对应字段 → 该段被 yaml **静默忽略**`conf.New` 未启用 strict 模式)。同时 `MicroService:` 整段缺失 → `Spec.MicroService == nil``service.Start` 既不注册服务也不发布匿名清单(`D:\work\bsm-sdk\core\service\service.go:66-90`)。所列 `feedback.Check.Hello/Updates``feedback.Data.Configure/Areas/Tags` 在本模块 proto 中不存在(`proto/feedback.proto:6-13` 只有 `Method`)→ 死配置。
- `proto/feedback.proto:119-122``IdentRequest` 消息未被任何 rpc 引用 → 死消息。
- 未发现显式 TODO/占位。
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/config/config.go:39` | `conf.NotNil(Spec.Service, Spec.Cache)` 未校验 `Databases`(与 ads 等模块同模式),配合 `with.Databases` 的 panic缺配置时表现为启动崩溃且报错不含配置项名。 |
| 中 | `internal/logic/method/modify.go:79-106` | 删子表 → 更新主表 → 建子表 三步**没有事务包裹**(对比 `add.go:69` 的单次 `Create` 由 GORM 自动开事务),任一步失败都会留下半成品数据。 |
| 低 | `internal/impl/impl.go:13-16` | `RedisService`/`EtcdService`/`MemorySerice` 初始化后本模块无任何使用点(死代码,同 ads。 |
| 低 | `test/add.http:9,16` | 用例 JSON 含 `"type"` 字段proto 无此字段)且末尾多一个逗号(非法 JSON无法直接执行`status` 未传。 |
| 低 | `test/rpc/rpc.go:3-23` | 整个 `main` 被注释,且引用不存在的 `pb.NewMethodClient`/`pb.Crc` → 死代码。 |
| 低 | 全模块 | **无任何 `*_test.go`**`test/rpc/rpc.go``package rpc` 下且函数体全注释),上述 IDOR 与附件唯一键冲突均无测试覆盖。 |
| 低 | `internal/logic/method/modify.go:56-59` | 图片 `identity` 缺省时生成新 UUID意味着"不传 identity 就重建",与"按 identity 幂等更新"的语义不一致;结合 6.2 的重复写入问题,同一图片会被覆盖或冲突。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| F1 | 高 | `Get`/`Delete`/`Remark` 不解析身份 + 四接口均无归属校验IDOR | 越权读取/删除/篡改他人反馈 |
| F2 | 高 | `Get`/`List` 返回 `email`/`phone`PII | 用户隐私泄露 |
| F3 | 高 | `Modify` 把主表 `identity` 改写为随机 UUID | 记录"消失"、外部引用断裂 |
| F4 | 高 | `Modify` 附件未赋 `identity` → 子表唯一键冲突 | 修改功能不可用 |
| F5 | 高 | 关联被 `Updates` 与显式 `Create` 写两遍 + 无事务 | 修改报错且数据半更新 |
| F6 | 中 | `List``agency` 分支绕过归属过滤;`auth.Identity` 为空时返回全量 | 机构级数据枚举 |
| F7 | 中 | `List`/`Get` 未 Preload 附件 | 接口返回与 proto 契约不符 |
| F8 | 中 | `Delete` 不清理子表 | 孤儿数据堆积 |
| F9 | 中 | 4 份 etc 配置不符合 SDK 约定,独立进程 Fatal匿名清单整段失效且引用不存在的服务 | 独立部署不可用、鉴权策略失真 |
| F10 | 中 | `status` 列重复定义(`Std_IICUDS.Status` 与自定义 `Status` | 建表丢索引、默认值语义漂移 |
| F11 | 低 | 死代码Redis/Etcd/Memory、`IdentRequest``test/rpc`)、非法 http 用例、无测试、无事务 | 可维护性 |
## 8. 修复建议(务实项)
1. **补齐归属校验(最高优先)**`Get`/`Delete`/`Remark``service.ParseMetaCtx(ctx, nil)``auth`,再按 `in.Identity` 取出记录,比对 `record.PassportIdentity == auth.Identity``Modify` 在现有 `auth` 基础上加同一比对。管理侧(跨用户操作)另设显式判断,不要用"任意有效 JWT"代表权限。
2. **`Modify` 不要重设 `identity`**:从更新结构体里移除 `Std_IICUDS.Identity`(参考 `Map` 更新或 `Select(...)` 只选允许字段),保留原 `identity`;同时不要把 `Std_Passport` 用调用方身份覆写(会造成归属转移)。
3. **消除重复关联写入**`modify.go:89``Updates` 前加 `.Omit(clause.Associations)`(或 `Select` 限定主表字段),只保留 `:95-106` 的显式 `Create`;二者只能留一个。
4. **补附件 `identity`**`modify.go:70-76``add.go:57-66` 对齐,为 `FeedbackAccessory``Identity: utils.UUID()`
5. **`Delete` 清理子表**:按 `item_identity = in.Identity` 先删 `feedback_images`/`feedback_accessory`,再删主表(与 `Modify` 的现有动作一致)。
6. **事务**:把 `modify.go:79-106` 的删/更新/建三步放进 `impl.DBService.Transaction(func(tx *gorm.DB) error {...})`
7. **`Preload` 补齐**`list.go:35``get.go:29` 增加 `Preload("Accessories")`
8. **`List` 权限收敛**`agency` 过滤仅对管理角色开放;普通用户强制附加 `passport_identity = auth.Identity``auth.Identity` 为空时直接返回拒绝而不是放宽条件。
9. **`Remark` 限制状态**:仅允许管理角色传 `status`,或把 `status` 从请求体移除、由服务端按流程计算。
10. **`Get`/`Delete`/`Remark` 返回值补 `code`**`StatusReply.Code` 填明确成功/失败码,避免调用方只能看 `message`
11. **配置修正**:把 `etc/feedback*.yaml` 改成 SDK 约定(`Service:`/`Port:`/`BindIP:`/`Cache:`/`Databases:` + `MicroService: {Enable, Anonymous}`),删除 `feedback.Check.*`/`feedback.Data.*` 等不存在的匿名项;`conf.NotNil``Databases`
12. **模型去重**`feedback_item.go` 保留一处 `status` 定义(建议保留自定义 `int32` 并显式写 `index`,或移除自定义字段改用 `Std_IICUDS.Status` 并统一类型)。
13. **清理与补测**:删 `impl.go` 中未使用的 Redis/Etcd/Memory 初始化、`proto/feedback.proto:119-122``IdentRequest``test/rpc/rpc.go`;修正 `test/add.http` 的非法 JSON`Get`/`Modify`/`Delete`/`Remark` 各写 1 条越权用例与 1 条正常用例。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
2026-09-22 21:15:34 +08:00
## 9. 整改记录2026-09-22
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| F1 | 高 | `Get`/`Delete`/`Remark` 不解析身份、四接口均无归属校验IDOR | 已修复:全部补 `service.ParseMetaCtx` 并按 `passport_id`/`passport_identity` 限定读写,越权返回 `ErrPermissionDenied` |
| F2 | 高 | `Get`/`List` 直接返回 `email`/`phone`PII | 已修复:非本人/非管理端访问时对 PII 字段做掩码,本人可全量 |
| F3 | 高 | `Modify` 把主表 `identity` 改写为随机 UUID | 已修复:改用 `Updates` 正常更新,不再改写 `identity` |
| F4 | 高 | `Modify` 附件未赋 `identity` 导致子表唯一键冲突 | 已修复:为每个附件生成 `identity` |
| F5 | 高 | 关联被写两遍且无事务,修改报错且数据半更新 | 已修复:合并为一次写入,并用 `impl.DBService.Transaction` 保证主表与附件同成功/同失败 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。