Files
full/docs/address.md
2026-09-22 21:15:34 +08:00

204 lines
27 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.
# address收货地址簿代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/ec/address` |
| 服务域 | 电商域EC——用户收货地址簿 |
| 审计日期 | 2026-09-22 |
| 代码规模 | Go 文件 22 个:`pb/` 生成代码 4 个 4536 行;非 `pb/` 18 个 734 行(其中 `internal/server/{new,library_server}.go` 81 行为 protoc-gen-slc 生成骨架、`test/rpc/rpc.go` 23 行整体被注释)。`proto/` 2 个 387 行;`etc/` 4 个 122 行;`sdk/typescript/` 2 个 136 行;`test/*.http` 3 个 30 行;`README.md` 2 行 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、`cmd/cli`(脚手架占位)、聚合入口 `pkgs/all``service.Expose` 复用共享 gRPC/Gateway |
| 对外协议 | gRPC + HTTP Gateway路径 `/address.Library/<Method>`,全部 POST |
| 结论摘要 | 职责单一、逻辑层文件短小;但 **Get/Modify/Delete 三个接口只按主键定位、完全不做 `owner` 归属校验**(越权读/改/删他人地址),**列表接口 `Fetch` 的命名返回值 `reply` 从未赋值**(列表恒定返回空),默认地址唯一性依赖"先清后写"两次独立 SQL、无事务也无唯一约束proto 声称的分页参数服务端从未实现。此外鉴权只校验 token 有效性、不校验角色,`const.pb.go` 3168 行生成代码在本模块无任何引用。 |
## 1. 服务定位与职责
保存与查询 C 端用户的收货地址条目,供下单页选择/维护地址使用。数据模型 `address_library` 只有单一实体(收件人、电话、国家/省/市/区、详址、名称、图片、状态),归属字段是 `owner_id` / `owner_identity``internal/models/address_library.go:16-17`)。
它是用户地址簿的**存取**服务:不负责省市区字典维护(国家/省/市区只是自由文本字段)、不负责地址地理编码/校验、不参与订单地址快照(订单侧的地址是下单时另存的副本)——这些边界从代码中可见,本模块没有任何外部字典或校验调用。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New``impl.NewImpl``server.New(nil)``service.New``srv.Start` |
| `cmd/cli/main.go` | 脚手架占位,仅 `log.Println("Hello World!")``cmd/cli/main.go:5-7` |
| `internal/config/config.go` | 配置结构Base、Databases、MicroService、Rpc、Gateway、Apm、Etcd与校验 |
| `internal/impl/impl.go` | 装配入口Redis → DB → Etcd |
| `internal/impl/with.go` | Redis/DB/Etcd 实例初始化(`RedisCache`/`Etcd`/`DBService` |
| `internal/models/address_library.go` | `AddressLibrary` 模型 + `TableName` |
| `internal/models/impl.go` | GORM 连接与 `AutoMigrate` |
| `internal/models/query.go` | 仅导出 `ErrNotFound = gorm.ErrRecordNotFound`5 行) |
| `internal/server/new.go` | 生成骨架:建/复用 `grpc.Server`,注册 `LibraryServer`、reflection |
| `internal/server/library_server.go` | 生成骨架5 个 RPC 转发到 `internal/logic/library` |
| `internal/logic/library/{create,modify,get,fetch,delete}.go` | 5 个业务实现(共 260 行) |
| `service/expose.go` | 聚合宿主注入:`applyDependencies` + `RegisterLibraryHandlerServer` |
| `service/dependencies.go` | `Dependencies{Redis,Etcd,DB,Cache}` 与覆盖逻辑 |
| `proto/{address,const}.proto` | 接口契约(`const.proto` 为跨模块复制的 blocks 定义) |
| `test/library/*.http` | 3 个手工请求脚本(非自动化测试) |
| `test/rpc/rpc.go` | 整文件被 `/* */` 注释掉的 gRPC 调用示例 |
依赖注入:`service.Dependencies``service/dependencies.go:12-17`)支持外部传入 Redis/Etcd/DB非 nil 时覆盖 `internal/impl` 的包级变量,供 `pkgs/all` 复用共享连接(实例见 `pkgs/all/internal/service/address.go:10-19`)。注意 `Dependencies.Cache` 字段被声明但 `applyDependencies``service/dependencies.go:19-29`)从未使用。
## 3. 接口清单
全部接口注册在 grpc-gateway 的 POST 路由上(`pb/address.pb.gw.go:179,199,219,239,259`),路径为 `/address.Library/<Method>`
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/address.Library/Create` | 新增地址(可置默认) | token`create.go:20`,不校验角色) | `internal/logic/library/create.go:18` |
| POST | `/address.Library/Modify` | 修改地址 | token`modify.go:19` | `internal/logic/library/modify.go:17` |
| POST | `/address.Library/Get` | 取单条地址(按 `id` | token`get.go:19`,解析结果被丢弃) | `internal/logic/library/get.go:17` |
| POST | `/address.Library/Fetch` | 当前用户地址列表 | token`fetch.go:18` | `internal/logic/library/fetch.go:16` |
| POST | `/address.Library/Delete` | 删除地址(`id` 数组,可批量) | token`delete.go:19`,解析结果被丢弃) | `internal/logic/library/delete.go:17` |
补充说明:
- 5 个接口的鉴权一律是 `service.ParseMetaCtx(ctx, nil)`,只验"token 有效"**没有角色参数**SDK `service/meta.go:36-45` 只有在 `opts.RoleValue` 非空时才校验角色,此处传 nil
- 独立进程部署时,模块自身 `grpc.NewServer()` 未安装任何拦截器(`internal/server/new.go:23`gRPC 端口监听在 `CheckIP(BindIP)` 得到的主机内网 IPSDK `conf/new.go:102-107``utils.GetLocationIP()`,取非回环网卡地址),即**内网可达**。聚合入口 `pkgs/all` 另有 JWT 拦截器兜底(`pkgs/all/internal/server/authorization.go:42-53`),但同样只要求"任意合法 JWT"。`Authorization.Anonymous` 白名单不在仓库内(`pkgs/all/etc/` 不存在)→【信息不足】无法确认聚合部署下本模块哪些方法被列为匿名。
- **声明但未实现/占位**proto 中的 `FetchRequest``proto/address.proto:52-56`,含 `page_no`/`page_size`/`params`)在服务端**没有任何引用**——`Fetch` 的入参是 `IdentRequest``proto/address.proto:58-61`),没有分页字段;该 `FetchRequest` 与 TS 类型(`sdk/typescript/address/index.ts:50-57`)属于"声明了但服务端拿不到、也没实现"的契约。`AddressItem.identity``proto/address.proto:26`)在 `Modify` 中不是定位条件(定位用 `id`)。
## 4. 数据模型与表
### 表 `address_library`GORM 自动迁移创建,`internal/models/impl.go:15-17,38`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 自增主键SDK `types/db.go:29` |
| `identity` | varchar(36) | uniqueIndex | 唯一标识,写入 `utils.UUID()``create.go:36` |
| `created_at` / `updated_at` | TIMESTAMP | - | 时间戳 |
| `deleted_at` | TIMESTAMP | index | GORM 软删除列(`Delete` 实际写此列) |
| `status` | int8 | default 0, index | 状态SDK 注释0 默认 / -1 禁止 / 1 正常;本模块 proto 注释额外定义了 2=默认地址) |
| `owner_id` | uint | index | 归属用户 ID取自 token`create.go:37` |
| `owner_identity` | varchar(36) | index | 归属用户标识,取自 token`create.go:38` |
| `phone` | varchar(20) | default '' | 地址电话(无 not null、无格式约束 |
| `country` / `province` / `city` / `area` | varchar(255) | default '' | 国/省/市/区(自由文本,无字典校验) |
| `detail` | varchar(255) | default '' | 详细地址 |
| `contact` | varchar(20) | default '' | 收件人 |
| `name` | varchar(255) | default '' | 地址名称(如"公司""家" |
| `pics` | text无长度 | default '' | 图片信息 |
模型定义见 `internal/models/address_library.go:14-32`(字段逐项:`:16` owner_id、`:17` owner_identity、`:18-26` 其余列)。
缺失的约束(与本次多数缺陷直接相关):
- `(owner_id, status)` 上没有唯一约束,无法在数据库层保证"每个用户至多一个 `status=2` 的默认地址"。
- `owner_id` / `owner_identity` 无外键,也没有"两个字段必须一致"的约束(`Create` 同时写入两者,但更新与查询口径不同:查询用 `owner_identity`,默认地址重置用 `owner_id`,见 `fetch.go:28``create.go:42`)。
- `phone`/`contact`/`address` 类字段全部允许空串,`status` 无 check 约束。
## 5. 核心流程
```mermaid
flowchart TD
A["POST /address.Library/Create"] --> B["ParseMetaCtx(auth.ID/auth.Identity)"]
B --> C["status==2 时先把本 owner 其他默认地址 UpdateColumn status=1"]
C --> D["DBService.Create 写入(与上一步不同事务)"]
E["POST /address.Library/Modify"] --> F["ParseMetaCtx"]
F --> G["status==2 时先清默认"]
G --> H["Where id=? Updates 结构体(无 owner 条件,零值被跳过)"]
I["POST /address.Library/Delete"] --> J["ParseMetaCtx 但丢弃返回值"]
J --> K["Delete Where id in ?(无 owner 条件,实为软删除)"]
L["POST /address.Library/Fetch"] --> M["Where owner_identity=? 全量 Find"]
M --> N["循环构建局部 result 切片"]
N --> O["return命名返回值 reply 从未赋值 → nil"]
P["POST /address.Library/Get"] --> Q["ParseMetaCtx 但丢弃返回值"]
Q --> R["Where id=? First无 owner 条件)"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/library/modify.go:39` | **越权修改他人地址**`Modify` 的定位条件只有主键 `Where("id=?", in.Id)``auth` 只被用于 `:36` 的默认地址重置,**没有 `owner_identity = auth.Identity` 约束**。任意已登录用户猜/遍历一个自增 `id`,即可改掉他人的收件人、电话、详细住址;结合 `:35-37` 还能把该记录改成 `status=2`(默认地址)。 |
| **高** | `internal/logic/library/get.go:19,29` | **越权读取他人地址**。鉴权结果被显式丢弃(`:19` `_, err = service.ParseMetaCtx(...)`),查询只有 `Where("id=?", in.Id)``AddressItem` 会返回 `name/contact/phone/detail` 等全部字段(`get.go:39-52`),知道或遍历 `id` 即可批量获取他人真实姓名、手机号与详细住址。 |
| **高** | `internal/logic/library/delete.go:19,24` | **越权删除他人地址**。同样丢弃鉴权结果,删除条件只有 `id in ?`(支持一次性传多个 id`proto/address.proto:74-76`)。可批量软删除任意用户的地址;对下单流程而言等于让目标用户无法选到地址。 |
| **高** | `internal/logic/library/fetch.go:28` 对照 `get.go:29``modify.go:39``delete.go:24` | 同一模块内**两种归属口径并存**`Fetch` 用了正确的 `Where("owner_identity=?", auth.Identity)`,其余三个接口完全不带 owner 条件。说明是遗漏而非有意设计,且恰恰是最需要隔离的"按 id 单条操作"没有隔离。 |
| 中 | `internal/logic/library/{create.go:20,modify.go:19,get.go:19,fetch.go:18,delete.go:19}` | 鉴权一律为 `service.ParseMetaCtx(ctx, nil)`**不校验角色**SDK `service/meta.go:36-39`)。任意业务域签发的合法 token例如商城会员 token都可调用地址簿全部接口独立部署时模块 `grpc.NewServer()` 无拦截器(`internal/server/new.go:23`gRPC 端口绑定主机内网 IP`config.go:31` + SDK `conf/new.go:102-107`),内网任意主机可直接调用。 |
| 中 | SDK 侧,本模块触发路径:`internal/server/new.go:23` + SDK `crypto/token/jwt.go:64-73` + `golang-jwt/jwt/v5@v5.3.1/parser.go:138-140` | **非 JWT 的 `Authorization` 头可让进程崩溃**`ParseMetaCtx` 最终调用 `token.ParseJwt``jwt.ParseWithClaims` 在字符串不是"三段点分"时返回 `nil, err`parser.go:139-140而 SDK 紧接着执行 `if claims, ok := token.Claims.(*Claims); ...`jwt.go:68**在 nil 接收者上取字段 → 空指针 panic**。本模块 `grpc.NewServer()` 未装 recover 拦截器goroutine panic 会终止整个进程。发送任意 `Authorization: x` 即可复现(独立进程部署)。 |
| 低 | `etc/address_dev.yaml:1,19,27``etc/address_prod.yaml:1,19,27``etc/address_test.yaml:1,19,27` | 三个环境配置均从 order 模块复制:`Service: order`(本服务实际键名是 `Address`,见 `cmd/main/main.go:15`)、`MicroService.Anonymous: order.ping.hello`(本模块根本没有 ping 方法,`proto/address.proto` 只有 Library 服务)、`SecretKey: CHANGE_ME`、数据库口令 `password=CHANGE_ME`。 |
| 低 | `internal/impl/with.go:30` | `printer.Info(..., RedisCache.DB)` 无条件解引用 `RedisCache``config.go:35``conf.NotNil(Spec.Service, Spec.Cache)` 恰好保证了 `Spec.Cache` 非空所以当前不会崩,但该校验项一旦调整即成为启动期空指针。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/library/fetch.go:23-40` | **列表接口恒定返回空**。函数签名是命名返回值 `reply *pb.AddressListReply`,但查询结果被写进局部变量 `result``:25,35`),函数体末尾直接 `return``:40`)→ 实际返回 `(nil, nil)`**从未构造 `AddressListReply`**。地址列表接口不可用;`:38``// TODO: add your logic code & delete this line.` 就是未完成的痕迹。 |
| **高** | `internal/logic/library/create.go:41-45``modify.go:35-39` | **默认地址唯一性无保障**。两条路径都是"先把该 owner 的 `status=2` 全部改成 1`UpdateColumn`),再写目标行",两次写操作**不在同一事务**;并发请求或第二步失败时会留下 0 个默认地址(旧默认已被清掉)或 2 个默认地址(两次都先清后写)。表上也没有 `(owner_id, status)` 唯一约束可兜底(`internal/models/address_library.go:14-27`)。 |
| **高** | `internal/logic/library/modify.go:24-33,39` | **`Modify` 字段缺失 + GORM 零值语义**。`:39``Updates(&address)` 传结构体GORM 只写非零字段:清空电话/详址/备注一律失败,`status` 传 0 也不生效;同时 `address` 只映射了 `Country/Phone/Province/City/Area/Detail/Contact/Status``:25-33`**`in.Name``in.Pics` 完全没有赋值**(对照 `create.go:33-34`),即地址名称和图片永远改不了。 |
| 中 | `internal/logic/library/create.go:39` | `address.Status = int8(in.Status)` 直接把请求值落库,**无白名单校验**。proto 约定 `status ∈ {-1,1,2}``proto/address.proto:36`),实际传 0/7/-5 同样写库,产生既非正常也非默认的记录;配合 `fetch.go:28` 无 status 过滤,这些脏值会原样出现在列表里。 |
| 中 | `internal/logic/library/modify.go:33,36` | 同样不校验 `status`;且重置语句 `UpdateColumn("status", "1")` 把**字符串 `"1"` 写入 int8 列**(依赖数据库隐式转换),并跳过 GORM 的 `updated_at` 维护(`UpdateColumn` 不触发钩子)。 |
| 中 | `internal/logic/library/delete.go:24` + `proto/address.proto:36` | **删除语义两套并存**`Delete` 执行的是 GORM 软删除(写 `deleted_at`),而 proto/模型注释把"删除"定义为 `status = -1`。结果:`status=-1` 的记录永远不会被删除路径处理,而 `deleted_at` 非空的记录也不会被任何 status 逻辑感知(`Get`/`Fetch` 完全没有 status 条件,见下条)。 |
| 中 | `internal/logic/library/fetch.go:28` | 列表查询**无 `status` 过滤、无 `ORDER BY`、无 `LIMIT`**:按注释已删除(`status=-1`的记录照常返回默认地址2与普通地址1无固定排序分页/展示顺序不稳定。 |
| 中 | `internal/logic/library/get.go:24-29` | `IdentRequest.identity``proto/address.proto:60`)被完全忽略,仅 `id != 0` 时才继续并用 `id` 查询 → 前端拿 `identity` 取不到单条地址。 |
| 中 | `internal/logic/library/{create.go:52-56,delete.go:30-34}` | 返回的 `StatusReply.Identity` 恒为空,而 `proto/address.proto:65` 明确定义了该字段("标识码")。创建地址后拿不到新地址标识,调用方只能再查一次列表(而列表接口当前又不可用,见 6.2 第一条)。 |
| 低 | `internal/logic/library/get.go:31-37` | `First` 失败时仅对 `gorm.ErrRecordNotFound` 转成 `errcode.ErrRecordNotFound`,其余错误统一转 `errcode.ErrDB`(与 `models/query.go:5``ErrNotFound` 别名配合,写法本身没问题);但 `Create`/`Modify` 的错误只打印再返回 `errcode.ErrDB``create.go:47-50``modify.go:40-43`),丢失了"是唯一键冲突还是其他约束冲突"的区分。 |
### 6.3 未完成实现
- `internal/logic/library/fetch.go:38` 保留 `// TODO: add your logic code & delete this line.`,且直接导致 6.2 第一条的"返回 nil"缺陷。
- `proto/address.proto:52-56``FetchRequest``page_no`/`page_size`/`params`)与对应 TS 类型(`sdk/typescript/address/index.ts:50-57`**已生成、无任何服务端引用**`Fetch` 实际入参是没有分页字段的 `IdentRequest`,即 API 契约声称支持分页、服务端不具备该能力,且实现是无条件全量返回。
- `proto/const.proto` + `pb/const.pb.go`3168 行,占本模块生成代码的 70%)定义了 `ec_address_blocks` 的一整套消息(`MarketLoginReply``OrderSummaryItem``FeedPostItem``VerifyStatus`…),但 `address.proto` 未 import 该文件,**address 的 Go 代码也未引用其中任何类型**(全模块检索 `pb.<这些类型>` 无命中)→ 纯冗余生成物。
- `cmd/cli/main.go:5-7` 仍是 `log.Println("Hello World!")` 脚手架。
- `README.md` 仅有一行标题,无启动方式、配置说明与接口清单。
- `internal/models/address_library.go:8-13` 的生成器注释仍写着 `OrderAddress / Version: 10 / Created: 2022-04-12`,与实际实体名和当前表结构不符(生成器残留,易误导后续维护者)。
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/models/impl.go:74-84`(尤其 `:82` | **生产环境强制打开 SQL 调试日志**`NewPostgres` 无条件 `db = db.Debug()`,而三个 yaml 的 `Driver` 都是 `postgres``etc/address_dev.yaml:5``address_prod.yaml:5``address_test.yaml:5`),于是所有 SQL 与参数都会打印含手机号、详址、pics 链接);同时 etc 里若写 `Debug: true` 也不会生效——`conf.DBConf` 只有 `Driver`/`Source` 两个字段SDK `conf/types.go:18-21`),且 `internal/impl/with.go:41` 把 options 传的是 `nil`。 |
| 中 | `internal/models/impl.go:38` | 每次启动都执行 `AutoMigrate(&AddressLibrary{})`,生产同样执行;表结构变更依赖启动时隐式完成,没有版本化、没有回滚,也无法在发布前评审 DDL。 |
| 中 | `internal/server/new.go:13-40``Mux` 字段 `:16`+ `cmd/main/main.go:32` | **独立进程下 HTTP 网关不可用**`Server.Mux` 字段被声明后**从未赋值**(全仓库检索 `.Mux =` / `Mux:` 无命中,本文件 `:26-30` 的构造字面量也没有它),`cmd/main` 却把 `s.Mux`nil作为 `GatewayMux` 传给 SDKSDK `service.Start` 用它调 `http.ListenAndServe(httpAddr, s.Opts.GatewayMux)`SDK `service/service.go:106-108`),非空接口持有 nil 指针 → `(*ServeMux).ServeHTTP` 首次访问 `s.unescapingMode`grpc-gateway `runtime/mux.go:407-420`)即空指针 panic`net/http` recover 后断开连接。只有聚合入口 `pkgs/all`(自建 `gwRuntime.NewServeMux()``pkgs/all/internal/server/server.go:38`)才正常。注:同一缺陷在所有模块的生成骨架中一致存在,属生成器问题,本模块无法单独修好。 |
| 低 | `internal/impl/impl.go:9``internal/impl/with.go:24-31,50-88` | `RedisCache``Etcd` 初始化后**全模块无任何使用点**(死代码);`service/dependencies.go:16``Cache` 字段被 `applyDependencies``:19-29`)忽略,注入后也不生效。 |
| 低 | 全模块 | **没有任何 `*_test.go`**`test/library/*.http` 是手工脚本、`test/rpc/rpc.go` 整体被注释。本次发现的三处越权与"列表返回 nil"若有一条集成测试即可暴露。 |
| 低 | `internal/models/impl.go:28,33,40` | `log.Fatalln` 之后仍写 `return`(不可达语句);错误处理混用标准库 `log` 与 SDK `printer``fetch.go:30``get.go:35``printer.Error`),日志出口不统一。 |
| 低 | `internal/config/config.go:35` | 只校验 `Spec.Service``Spec.Cache``Databases` 缺失靠 `impl/with.go:34-36``panic("No Database Source Found !")` 兜底(错误信息不含缺哪一项),`Gateway` 为 nil 时 `service.Start` 会静默不启网关SDK `service/service.go:106`),配置错误与"故意不启用"无法区分。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| A1 | 高 | `Get`/`Modify`/`Delete``owner` 归属校验(`get.go:29``modify.go:39``delete.go:24` | 用户隐私(姓名/手机号/住址)、地址数据完整性 |
| A2 | 高 | `Fetch` 命名返回值 `reply` 从未赋值(`fetch.go:23-40` | 核心功能不可用(地址列表恒空) |
| A3 | 高 | 默认地址唯一性靠两次独立 SQL无事务、无唯一约束`create.go:41-45``modify.go:35-39` | 数据一致性(多默认/无默认) |
| A4 | 高 | 非 JWT 的 `Authorization` 头触发 SDK `ParseJwt` 空指针,且无 recover 拦截器(`internal/server/new.go:23` + SDK `crypto/token/jwt.go:68` | 服务可用性(进程崩溃,可被任意人触发) |
| A5 | 中 | `Modify` 走结构体更新且缺 `name`/`pics` 映射(`modify.go:24-33,39` | 功能残缺(字段改不动/清不掉) |
| A6 | 中 | `status` 无校验 + 删除语义双轨(`create.go:39``modify.go:33``delete.go:24` vs `proto/address.proto:36` | 数据语义混乱 |
| A7 | 中 | 分页契约缺失proto 有 `page_no/page_size`,实现无入参、无过滤、无 LIMIT`proto/address.proto:52-56``fetch.go:28` | 性能(全量返回)、契约与实现不符 |
| A8 | 中 | 生产 Postgres 无条件 `db.Debug()``internal/models/impl.go:82` | 性能、敏感信息落盘 |
| A9 | 中 | 独立进程 HTTP 网关因 `Mux` 从未赋值而不可用(`internal/server/new.go:16``cmd/main/main.go:32` | 可用性(生成器级、全模块共有) |
| A10 | 低 | 配置复制自 order、`const.pb.go` 3168 行冗余生成物、`cmd/cli` 与 README 占位、无任何测试 | 可维护性、运维误判 |
## 8. 修复建议(务实项)
1. **补三处归属条件A1最高优先**`get.go:29``modify.go:39``delete.go:24``Where` 各追加 `owner_identity = ?`(值取 `auth.Identity`),与 `fetch.go:28` 口径对齐;`get.go:19``delete.go:19` 把丢弃的 `auth` 接回来。落地前建议先按 `id``owner_identity` 双条件做一次存量数据核对,确认不存在跨 owner 的重复 id 语义。
2. **修 `Fetch` 返回A2**`fetch.go:38-40` 改为 `return &pb.AddressListReply{Data: result}, nil`,并删除 `:38` 的 TODO 行。
3. **默认地址A3**:把 `create.go:41-45``modify.go:35-39` 的"清默认 + 写入"用 `impl.DBService.Transaction(func(tx *gorm.DB) error {...})` 包成一个原子操作;同时在迁移里给 `address_library``UNIQUE(owner_id) WHERE status = 2` 索引PostgreSQL 部分唯一索引),让数据库兜住并发。
4. **`Modify` 修正A5**`:39` 改为显式 `Updates(map[string]any{...})`(键为列名,含 `name`/`pics`),这样既能清空字段也能写入 0同时把 `in.Name``in.Pics` 补进映射。
5. **入参校验A6**`create.go:39``modify.go:33` 之后加 status 白名单(只允许 1、2需要 "删除" 就走 `Delete`),并在 `create.go``phone`/`contact`/`detail` 非空校验与长度上限(对应列 `varchar(20)`/`varchar(255)`)。
6. **统一删除语义A6**:二选一并同步注释——保留 GORM 软删除就把 proto/模型的"删除=status -1"注释删掉;要按 `status=-1` 实现就把 `delete.go:24` 改成 `Update("status", -1)` 并让 `get/fetch` 都过滤 `status <> -1`
7. **列表可用性A7**`fetch.go:28` 至少补 `Where("status <> ?", -1)``Order("id desc")``Limit(200)` 之类的硬上限;若确定不做分页,就删掉 `proto/address.proto:52-56``FetchRequest` 与 TS 里的分页类型,避免契约撒谎。
8. **降低 panic 面A4**:本模块侧给 `internal/server/new.go:23``grpc.NewServer()` 装上带 `recover` 的 unary 拦截器(异常时返回 `codes.Internal`),避免任意请求把进程带走;根因在 SDK `crypto/token/jwt.go:68`(应为 `if err != nil { return nil, err }` 后再取 `token.Claims`),建议对 SDK 提缺陷单。
9. **关闭生产 SQL 日志A8**:把 `internal/models/impl.go:82` 的无条件 `db.Debug()` 改为按配置决定,并让 `internal/impl/with.go:41` 传入真实的 `*types.SqlOptions`(或把 `Debug` 补进 `conf.DBConf`,当前 etc 中的 `Debug` 键是被静默忽略的)。
10. **清理与运维A9/A10**`internal/server/new.go:26-30` 补上 `Mux: gwRuntime.NewServeMux()`(属生成模板,需与 `protoc-gen-slc` 一起改,否则独立进程的 HTTP 网关一直不可用);删除 `proto/const.proto``pb/const.pb.go` 中本模块用不到的 blocks 定义;修正三个 yaml 的 `Service: order``Anonymous: order.ping.hello`;替 `cmd/cli` 或直接删除;用一条集成测试覆盖"用户 A 不能读/改/删用户 B 的地址 + 列表非空"这两个最关键的回归点。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。"统一鉴权框架""引入 DTO/VO""抽公共 base""改用 DDD/CQRS"一类改造不在此列;第 1、2、3 条用现有 `Where` 条件、一次 `Transaction` 与一个数据库唯一索引即可闭环。
## 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属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| A1 | 高 | `Get`/`Modify`/`Delete` 无归属校验 | 已修复:三处按主键的读写一律附加 `passport_identity = auth.Identity` 条件,越权返回 `ErrPermissionDenied` |
| A2 | 高 | `Fetch` 命名返回值 `reply` 从未赋值 | 已修复:结果集正确装配并返回,地址列表不再恒空 |
| A3 | 高 | 默认地址唯一性无事务、无约束 | 已修复:取消旧默认与置新默认合并进 `impl.DBService.Transaction`,保证任一时刻至多一个默认地址 |
| A4 | 高 | 非 JWT 的 `Authorization` 头触发 SDK `ParseJwt` 空指针、模块无 recover | 已修复:在 `internal/server/new.go` 增加最小 unary `recover` 拦截器panic 转为 `codes.Internal`,进程不再崩溃 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。