777 lines
65 KiB
Markdown
777 lines
65 KiB
Markdown
# pkgs/ecmall 开发上下文
|
||
|
||
本文面向需要在 `pkgs/ecmall` 或其关联子模块上继续开发的工程师,说明这个聚合入口是什么、代码怎么组织、请求怎么流转、数据放在哪里、哪些实现是缺口,以及改动时的注意事项。
|
||
|
||
其他文档的分工:模块职责概览见 [根 README](../../README.md),聚合入口与请求分流的实现见 [系统架构](../../wiki/architecture.md),配置/构建/发布流程见 [开发与运维](../../wiki/operations.md),逐接口的请求字段见 [API 接入总览](../../wiki/api/00-overview.md) 与 `wiki/api/01-*.md` ~ `wiki/api/18-*.md`。
|
||
|
||
> 本文所有结论均来自当前工作区源码。带 `文件:行` 的引用表示已核对到具体位置;标注「推断」的表示由代码推导而未直接验证。
|
||
|
||
---
|
||
|
||
## 1. 定位与边界
|
||
|
||
`pkgs/ecmall` 是**面向电商业务的聚合宿主**(module 路径 `bsm/full/pkgs/ecmall`),监听 gRPC `12300` / HTTP `12301`。它把电商交易、资金支付、平台运营和它们依赖的基础支撑服务装配进同一个进程,共享同一套监听、鉴权、grpc-gateway、动态 RPC 与配置装配实现。
|
||
|
||
注册的 14 个服务:
|
||
|
||
| 分组 | 服务 | 子模块 |
|
||
| --- | --- | --- |
|
||
| 基础支撑 | initial、passport、sender | `module/base/*` |
|
||
| 平台与运营 | ads、cms、feedback、fts、logs、mgt | `module/base/*` |
|
||
| 电商与交易 | address、mall、market、order | `module/ec/*` |
|
||
| 资金与支付 | wallet | `module/finance/wallet` |
|
||
|
||
对外接口面:11 个模块注册 **35 个 gRPC 服务**(同时可用原生 gRPC、grpc-gateway 与动态 RPC),`fts`、`logs`、`mgt` 三个模块**只有原生 REST 路由**(`/rest/fts`、`/rest/logs`、`/rest/mgt`),不注册任何 gRPC 服务。专项配置节为 `Fts`、`Mgt`、`Passport`、`Sender`、`Wallet`;`go.mod` 中有 14 个业务模块的本地 `replace`。
|
||
|
||
**未接入**:`module/base/cloud`(唯一未注册的 base 模块,其路由与专项配置在 ecmall 中完全不存在),以及全部 `module/social/*`(feed、group、relation 未接入任何聚合入口)。
|
||
|
||
关键约束:**`pkgs/ecmall` 只是注册表的取舍,不复制任何子模块业务逻辑**。任何功能改动都应落在 `module/` 下的对应模块里,`pkgs/ecmall` 只改注册表、配置结构或聚合自身的服务端代码。
|
||
|
||
---
|
||
|
||
## 2. 代码地图
|
||
|
||
```
|
||
pkgs/ecmall/
|
||
├── cmd/main/main.go 聚合进程入口(--workspace 参数、信号处理)
|
||
├── etc/default_dev.yaml 开发环境配置样例(端口、数据库、服务列表、专项配置)
|
||
├── go.mod / go.sum 14 个本地 replace + SDK
|
||
└── internal/
|
||
├── config/config.go 聚合配置结构与校验
|
||
├── config/config_test.go 配置测试(校验服务集合与必需配置节)
|
||
├── impl/impl.go 共享 DB / Redis / etcd / 内存缓存
|
||
├── server/ 聚合服务端(与业务无关的基础设施)
|
||
│ ├── server.go gRPC + HTTP 生命周期、gateway/REST 分流
|
||
│ ├── authorization.go JWT 鉴权(gRPC 拦截器 + HTTP 中间件)
|
||
│ ├── dynamic.go 动态 RPC(反射 + dynamicpb)
|
||
│ └── response.go 统一错误响应
|
||
└── service/ 注册表与依赖注入(每服务一个文件)
|
||
├── service.go 注册表 + Expose 主流程
|
||
├── service_test.go 注册表冒烟测试(gRPC 服务 + REST 路由)
|
||
├── ads.go cms.go feedback.go fts.go
|
||
├── initial.go logs.go mgt.go passport.go sender.go
|
||
└── address.go mall.go market.go order.go wallet.go
|
||
```
|
||
|
||
`internal/server` 与 `internal/service` 之外的部分都是模板化的;真正的业务代码全部在 `module/` 里。
|
||
|
||
---
|
||
|
||
## 3. 启动与配置
|
||
|
||
### 3.1 启动顺序
|
||
|
||
`pkgs/ecmall/cmd/main/main.go`:
|
||
|
||
1. 解析 `--workspace`(默认 `default`),经 `utils.MustString` 校验
|
||
2. `config.New(ServiceKey)` → 读配置、校验监听地址/密钥/有效期、回填共享配置
|
||
3. `impl.NewImpl()` → 创建**进程级共享**的内存缓存、Redis、数据库、etcd 客户端
|
||
4. `server.New(key, expire, anonymous)` → 建 gRPC server(挂鉴权拦截器 + reflection)与 Gin engine
|
||
5. `service.Expose(srv)` → 遍历注册表,对启用的服务 `applyDependencies` + 注册 gRPC/gateway
|
||
6. `srv.Start(grpcAddr, httpAddr)` → 监听并阻塞
|
||
7. `SIGINT`/`SIGTERM` → `srv.Stop(ctx)`,先 `GracefulStop`,10 秒超时后强制 `Stop`,再关 HTTP(`cmd/main/main.go:46-57`)
|
||
|
||
### 3.2 配置解析规则
|
||
|
||
由 SDK `conf.New` 决定(`D:\work\bsm-sdk\core\conf\new.go:25-67`):
|
||
|
||
```
|
||
<BSM_Prefix>/etc/<小写 serviceKey>_<BSM_RuntimeMode>.yaml
|
||
```
|
||
|
||
- 文件不存在时回退到 `<BSM_Prefix>/etc/workspace_<BSM_Workspace>_<Mode>.yaml`(`new.go:39-43`)
|
||
- YAML 内容会经 `os.ExpandEnv`,支持 `${VAR}` 替换(`new.go:55`)
|
||
- YAML 中**必须**包含 `Service:` 键,否则 `log.Fatalln`(`new.go:58-60`)
|
||
- 解析使用非严格模式,**未知键被静默忽略**(`new.go:63`)
|
||
|
||
因此 `--workspace=tenant01` + `BSM_RuntimeMode=dev` 读的是 `pkgs/ecmall/etc/tenant01_dev.yaml`。
|
||
|
||
### 3.3 环境变量
|
||
|
||
| 变量 | 默认值 | 作用 |
|
||
| --- | --- | --- |
|
||
| `BSM_RuntimeMode` | `dev`(转小写) | 决定配置文件名后缀;prod 时 `BSM_Prefix` 默认 `/usr/local/bsm` |
|
||
| `BSM_Prefix` | dev: 进程工作目录;否则 `/usr/local/bsm` | 配置与证书根目录 |
|
||
| `BSM_Workspace` | `default` | 回退配置的文件名 |
|
||
| `BSM_SERVICES` | 空 | **聚合专用**,非空时覆盖 `Services` 列表 |
|
||
| `BSM_JwtSecretKey` | `Cblocksmesh2022C` | 被 `config.New` 用 `Authorization.Key` 覆盖 |
|
||
| `BSM_Licence` | 空 | 许可文件路径 |
|
||
|
||
(`D:\work\bsm-sdk\core\env\env.go:15-35`;`BSM_SERVICES` 见 `internal/config/config.go:109-124`)
|
||
|
||
### 3.4 ecmall 的配置节
|
||
|
||
| 节 | 作用 |
|
||
| --- | --- |
|
||
| `Server.GRPC` / `Server.HTTP` | 两段监听地址,相同会 panic |
|
||
| `Authorization` | JWT 密钥(必须 16/24/32 字节)、有效期秒数、匿名方法白名单 |
|
||
| `Databases` | **全进程共用**的数据库连接(`Driver` + `Source`) |
|
||
| `Cache` | 共享 Redis DSN |
|
||
| `SecretKey` | 被 `service.Expose` 用作 session 密钥(`internal/service/service.go:29`) |
|
||
| `Services` | 启用哪些注册表服务;值里出现 `all` 表示全启用 |
|
||
| `Fts` / `Mgt` | fts 与 mgt 的专项配置(对象存储/本地上传、初始化 root 用户)。聚合启动时把共享的 Base/Databases/Rpc/APM/Etcd 回填进这两个结构(`internal/config/config.go:92-97`),**不含 `MicroService`** |
|
||
| `Passport` / `Sender` / `Wallet` | 三个模块的专项配置,聚合启动时把共享的 Base/Databases/MicroService/Rpc/APM/Etcd 回填进这三个结构(`internal/config/config.go:98-106`) |
|
||
|
||
`ads`、`cms`、`feedback`、`logs` **没有**专项配置节:它们的 `service` 包不暴露 `Config` 类型,聚合无法向它们注入任何模块级配置,只能注入共享依赖。`Fts`、`Mgt` 两个节在 `etc/default_dev.yaml` 中必须存在,否则 `config_test.go` 直接失败。
|
||
|
||
`Services` 与 `BSM_SERVICES` 的匹配是大小写不敏感的,`BSM_SERVICES` 优先(`internal/config/config.go:109-124`)。
|
||
|
||
### 3.5 ⚠️ 两个必须知道的坑
|
||
|
||
**(1)聚合会把所有子模块摁到同一个数据库上。**
|
||
|
||
各模块独立运行时的开发配置指向**不同的库**:
|
||
|
||
| 模块 | 独立 dev 配置的 dbname | 独立 dev 配置的 Redis |
|
||
| --- | --- | --- |
|
||
| ads | `rst_dev` | `127.0.0.1:6379` |
|
||
| cms | `ec_mall` | `127.0.0.1:6379` |
|
||
| feedback | `milu` | `127.0.0.1:6379` |
|
||
| fts | `bsm_dev` | `127.0.0.1:6379/0` |
|
||
| initial | `rst_dev` | `127.0.0.1:6379` |
|
||
| logs | **达梦** `DAMENG`(`Driver: dm`) | `127.0.0.1:6379/0` |
|
||
| mgt | `ec_dev`(端口 19432) | `127.0.0.1:19379/0` |
|
||
| passport | `ec_dev`(端口 19432) | `127.0.0.1:19379` |
|
||
| sender | `bsm_dev` | `127.0.0.1:6379` |
|
||
| address / mall / market / order | `ec_mall` | `127.0.0.1:6379` |
|
||
| wallet | 模板值 `"{ServiceKey}"` | `127.0.0.1:6379` |
|
||
|
||
而 `pkgs/ecmall/etc/default_dev.yaml` 用 Postgres 的 `dbname=bsm_dev`。聚合模式下所有子模块共用这一个 `*gorm.DB` 和同一个 Redis DSN,**与各自独立运行时的目标库、甚至驱动都不一致**(`logs` 独立时走达梦,`feedback` 走 `milu`)。部署前必须确认目标库里这 14 个模块的表都在。
|
||
|
||
**(2)聚合启动不会建表,也不会跑 seed。**
|
||
|
||
- 聚合走 SDK `with.Databases(cfg, nil)`(`internal/impl/impl.go:38`)。SDK 仅在 `options.IsAutoMigrate` 为真时执行迁移,而该默认值是 `false`(`D:\work\bsm-sdk\core\database\sql\postgresql.go:17`、`D:\work\bsm-sdk\core\database\new.go:43`),且 `conf.DBConf` 只暴露 `Driver`/`Source` 两个键,**无法从 YAML 打开**(`D:\work\bsm-sdk\core\conf\types.go:18-21`)。
|
||
- 第一套(`database.AppendMigrate` / `database.MigrateTables` 登记):`ads`(`ads_item.go:38`、`ads_pos.go:22`)、`cms`(9 个模型各自 `init()`,如 `cms_accessory.go:22`)、`feedback`(3 个模型各自 `init()`)、`fts`(`fts_record.go:40`)、`logs`(`log_data.go:24`),以及 `initial`、`passport`、`sender`、`mall`、`wallet`。由于上面那条默认值,**这套登记在独立模式与聚合模式下都不会真正建表**。
|
||
- 第二套(不设开关的 `DBService.AutoMigrate(migrateTables...)`):`address`、`market`、`order`,以及 **`mgt`**(`module/base/mgt/internal/models/impl.go:61`,其后紧跟 `InitRootUserData()`)。它们只在模块自己的 `impl.NewImpl()` / `models.New` 里被调用,而聚合只调用 `applyDependencies`、从不调用模块的 `impl.NewImpl()`(见 §6)——**所以聚合模式下同样不建表**。
|
||
- 各模块的 `InitData()`(默认店铺/默认钱包/默认 CMS 根分类/mgt root 用户等 seed)都挂在独立入口的 `srv.Use(...)` 或 `models.New` 上,聚合路径不执行。`passport` 与 `relation` 的这行甚至是注释掉的(`module/base/passport/cmd/main/main.go:37`、`module/social/relation/cmd/main/main.go:37`)。
|
||
|
||
**结论:表结构必须在启动 ecmall 之前由独立模式启动一次、迁移脚本或 DBA 建好;mgt 的管理员账号也需要另行播种。**
|
||
|
||
---
|
||
|
||
## 4. 请求链路与鉴权
|
||
|
||
### 4.1 四种入口,ecmall 全都有
|
||
|
||
| 入口 | 形态 | ecmall 是否可用 |
|
||
| --- | --- | --- |
|
||
| 原生 gRPC | `/{package}.{Service}/{Method}` | ✅ 35 个服务 |
|
||
| grpc-gateway | `POST /{package}.{Service}/{Method}` | ✅ 同 35 个服务 |
|
||
| 动态 RPC | `POST /rpc/{package}/{Service}/{Method}` | ✅ 对全部已注册的 gRPC 服务可用 |
|
||
| 原生 REST(Gin) | `/rest/{module}/...` | ✅ 仅 `fts`、`logs`、`mgt`(约定入口) |
|
||
|
||
`/rest/fts`、`/rest/logs`、`/rest/mgt` 来自这三个模块的 `internal/routers`,由 `internal/service/{fts,logs,mgt}.go` 把聚合的 `srv.HTTP` 作为 `Engine` 传入后注册;**其余 11 个模块都没有 `internal/routers` 目录**,因此 ecmall 的原生 REST 面就等于这三个模块的路由集合(`/rest/fts/ping|config|uploader`、`/rest/logs/ping|create|fetch|total`、`/rest/mgt/*`)。
|
||
|
||
### 4.2 分流实现
|
||
|
||
`internal/server/server.go:63-71`:请求先交给 grpc-gateway,响应被 `bufferedResponse` 缓冲;**仅当 gateway 返回 404** 时才把同一请求交给 Gin。Gin 侧只注册了一条动态路由 `POST /rpc/:module/:service/:method`(`server.go:61`)。
|
||
|
||
监听参数:`ReadHeaderTimeout=10s`、`IdleTimeout=120s`、`MaxHeaderBytes=1MB`(`server.go:72-78`)。
|
||
|
||
### 4.3 动态 RPC 机制
|
||
|
||
`internal/server/dynamic.go`:
|
||
|
||
- 通过 gRPC **反射**(`grpc_reflection_v1`)按 `package.Service` 拉取 `FileDescriptorProto`,用 `protodesc` 解析后缓存方法描述符(`dynamic.go:115-176`)
|
||
- 用 `dynamicpb` 构造请求/响应,JSON ↔ protobuf 走 `protojson`(`dynamic.go:94-112`)
|
||
- **只支持 unary**,streaming 明确报错(`dynamic.go:79-82`)
|
||
- 请求体上限 4 MiB(`dynamic.go:30`、`:89-92`)
|
||
- 只透传 `authorization`、`x-request-id` 和所有 `x-` 前缀头到下游 gRPC(`dynamic.go:178-190`)
|
||
- 需要本进程开启 gRPC reflection,而 `server.New` 无条件 `reflection.Register`(`server.go:33`)
|
||
|
||
### 4.4 鉴权
|
||
|
||
`internal/server/authorization.go`:
|
||
|
||
- gRPC:unary 拦截器,非匿名方法必须带 `authorization` metadata(`authorization.go:42-53`)
|
||
- HTTP:中间件校验 `Authorization` 头(`authorization.go:55-66`)
|
||
- Token 为 **HS256 JWT,直接放在 `Authorization` 头,不加 `Bearer`**(加前缀会被判为解析失败)
|
||
- 校验项:签名算法必须 HS256、必须有 `exp`、必须有 `iat`、`iat` 不能在将来、`now - iat` 不得超过配置的 `Expire`(`authorization.go:68-94`)
|
||
- 路径规范化:`/rpc/passport/Login/Pwd` 会被改写为 `/passport.Login/Pwd` 再去查白名单(`authorization.go:105-112`)——**白名单只写网关形式即可同时覆盖动态 RPC**
|
||
- `grpc.reflection.*` 与 `grpc.health.*` 前缀始终匿名(`authorization.go:98-100`)
|
||
|
||
### 4.5 匿名白名单现状
|
||
|
||
`pkgs/ecmall/etc/default_dev.yaml` 当前列了:
|
||
|
||
- `passport.Login/Pwd`、`passport.Login/Code`、`passport.Login/Quick`
|
||
- `passport.Register/Pwd`、`passport.Register/Code`
|
||
- `passport.Forget/Verify`、`passport.Forget/Reset`
|
||
- `passport.Verify/Request`、`passport.Verify/JumioCallback`
|
||
- `market.Agency/Login`、`mall.Staff/Login`
|
||
- `/rest/fts/ping`、`/rest/fts/config`、`/rest/logs/ping`
|
||
- `/rest/mgt/ping`、`/rest/mgt/session`、`/rest/mgt/login`、`/rest/mgt/refresh`、`/rest/mgt/reset`
|
||
|
||
REST 条目必须写成 `path.Clean` 后的**真实路径**(`/rest/logs/ping`),不会被改写成网关形式;`/rpc/...` 才会被规范化成 `/{package}.{Service}/{Method}`(见 §4.4)。
|
||
|
||
**注意**,以下几组接口在模块自己的配置/实现里是放开的,但 ecmall 的宿主鉴权**不会**自动跟随:
|
||
|
||
| 接口 | 模块自己的行为 | ecmall 中的行为 |
|
||
| --- | --- | --- |
|
||
| `ads.Fetch/ByPos` | dev 配置里列为匿名(`module/base/ads/etc/ads_dev.yaml:16`) | 需要 JWT |
|
||
| `sender.Sms/Send`、`sender.Sms/Verify`、`sender.Mail/Send` | dev 配置里匿名 | 需要 JWT |
|
||
| `feedback.Method/Get`、`Delete`、`Remark`、`List` | 逻辑内部不鉴权、不校验归属(§9.3) | 需要 JWT(宿主拦截),但通过后仍无归属校验 |
|
||
| logs 的 `POST /rest/logs/create`、`fetch`、`total` | 模块内 JWT 中间件被注释 | ecmall 白名单**未**列这三条 → 需要 JWT(`/rest/logs/ping` 已列入白名单) |
|
||
|
||
如果希望保持独立运行时的行为,必须显式加入白名单。
|
||
|
||
### 4.6 错误响应
|
||
|
||
统一为 `{code, message, details, timeseq}`,HTTP 状态码恒为 200,`code` 为 gRPC status code(`internal/server/response.go:66-77`)。`sdkError` 会保留 SDK 自定义业务码(`response.go:18-24`)。
|
||
|
||
---
|
||
|
||
## 5. 服务注册表(35 个 gRPC 服务 + 3 个 REST-only 模块)
|
||
|
||
| 模块 | gRPC 服务 | 通过 `ExposeOptions.Config` 传入配置 | 注册方式 |
|
||
| --- | --- | --- | --- |
|
||
| ads | `Fetch`(1) | 否 | gRPC + gateway |
|
||
| cms | `Category`(4)、`Pages`(6)、`Post`(19)、`Site`(5)、`Tags`(4) | 否 | gRPC + gateway |
|
||
| feedback | `Method`(6) | 否 | gRPC + gateway |
|
||
| fts | **无** | ✅ `Fts` | **仅原生 REST** `/rest/fts` |
|
||
| initial | `Check`、`Data` | 否 | gRPC + gateway |
|
||
| logs | **无** | 否 | **仅原生 REST** `/rest/logs` |
|
||
| mgt | **无** | ✅ `Mgt` | **仅原生 REST** `/rest/mgt` |
|
||
| passport | `Account`、`Forget`、`Login`、`Register`、`Verify` | ✅ `Passport` | gRPC + gateway |
|
||
| sender | `Mail`、`Sms` | ✅ `Sender` | gRPC + gateway |
|
||
| address | `Library` | 否 | gRPC + gateway |
|
||
| mall | `Ads`、`Category`、`Freight`、`Notice`、`Product`、`Staff`、`Store` | 否 | gRPC + gateway |
|
||
| market | `Agency`、`Data`、`Supply` | 否 | gRPC + gateway |
|
||
| order | `Cart`、`Coupon`、`Mgt`、`Summary` | 否 | gRPC + gateway |
|
||
| wallet | `Alipay`、`Basic`、`Payment`、`Wechat` | ✅ `Wallet` | gRPC + gateway |
|
||
|
||
`fts`、`logs`、`mgt` 三个模块的 `ExposeOptions` 只有 `Dependencies` + `Engine`(`*gin.Engine`),**没有 `GRPC`/`Gateway` 字段**,因此它们既不出现在原生 gRPC 面,也不出现在 gateway 与动态 RPC 面,只能通过 `/rest/...` 调用。
|
||
|
||
注册来源:模块的 `service/expose.go`(gateway / REST)与 `internal/server/new.go`(gRPC)。逐方法清单见 `wiki/api/01-*.md` ~ `wiki/api/18-*.md`;注意无需手写 `google.api.http` 注解,所有 gRPC 模块的网关路径都是 grpc-gateway 默认的 `/{package}.{Service}/{Method}`(全部 POST)。
|
||
|
||
改注册表的正确做法:在 `internal/service/` 加/删一个 `exposeXxx` 文件,并在 `internal/service/service.go:14-32` 的 `services` 表里增删一行;同时同步 `internal/config/config_test.go` 的 `ecmallServices` 期望值与 `etc/default_dev.yaml` 的 `Services` 列表(该测试会强制三者一致)。`internal/service/service_test.go` 还会断言各模块的 gRPC/REST 面确实注册成功,漏接线会直接失败。
|
||
|
||
---
|
||
|
||
## 6. 依赖注入机制
|
||
|
||
每个模块的 `service/expose.go` 都长这样:
|
||
|
||
```go
|
||
type ExposeOptions struct {
|
||
Dependencies
|
||
GRPC *grpc.Server // 仅 gRPC 模块
|
||
Gateway *gwRuntime.ServeMux // 仅 gRPC 模块
|
||
Engine *gin.Engine // 仅 fts / logs / mgt(原生 REST)
|
||
Config *Config // 仅 fts / mgt / passport / sender / wallet
|
||
}
|
||
|
||
func Expose(options ExposeOptions) error {
|
||
applyDependencies(options.Dependencies)
|
||
if options.Config != nil { config.Spec = *options.Config }
|
||
server.New(options.GRPC) // 仅 gRPC 模块
|
||
routers.Register("mgt", options.Engine) // 仅 REST 模块
|
||
// ... 或注册 grpc-gateway handler
|
||
}
|
||
```
|
||
|
||
`applyDependencies` 把聚合进程的共享对象写进模块自己的 `internal/impl` 包变量:
|
||
|
||
| 模块 | Redis | Etcd | DB | Cache |
|
||
| --- | --- | --- | --- | --- |
|
||
| ads | ✅ | ✅ | ✅ | ⚠️ 写入 `impl.MemorySerice`,但全模块无读取点 |
|
||
| cms | ✅ | ✅ | ✅ | ⚠️ 写入 `impl.MemoryService`,但无读取点 |
|
||
| feedback | ✅ | ✅ | ✅ | ⚠️ 写入 `impl.MemorySerice`,但无读取点 |
|
||
| fts | ✅ | ✅ | ✅ | ⚠️ 写入 `impl.MemorySerice`,但无读取点 |
|
||
| initial | ✅ | ✅ | ✅ | ✅ → `impl.MemorySerice` |
|
||
| **logs** | ✅ | ✅ | ✅ | ❌ **完全忽略**(连 `impl` 变量都不存在) |
|
||
| mgt | ✅ | ✅ | ✅ | ⚠️ 写入 `impl.MemorySerice`,但无读取点 |
|
||
| passport | ✅ | ✅ | ✅ | ✅ → `impl.MemoryService` |
|
||
| sender | ✅ | ✅ | ✅ | ✅ → `impl.MemorySerice` |
|
||
| **address** | ✅ | ✅ | ✅ | ❌ **接收但未使用** |
|
||
| mall | ✅ | ✅ | ✅ | ✅ → `impl.MemorySerice` |
|
||
| **market** | ✅ | ✅ | ✅ | ❌ **接收但未使用** |
|
||
| **order** | ✅ | ✅ | ✅ | ❌ **接收但未使用** |
|
||
| wallet | ✅ | ✅ | ✅ | ✅ → `impl.MemorySerice` |
|
||
|
||
即:`Dependencies.Cache` 在 address / market / order 中是死字段,在 logs 中根本没有落点;ads / cms / feedback / fts / mgt 虽然写入了 `impl` 变量,但模块内没有任何读取点——**往这 9 个模块注入内存缓存都不会产生业务效果**。另外多数模块把该字段拼成 `MemorySerice`(少一个 `v`),属于既有拼写,改名要连 `internal/impl` 一起动。
|
||
|
||
`applyDependencies` 只在值非 nil 时覆盖,所以模块的包变量在聚合模式下**始终**是聚合注入的对象;模块自己的 `impl.NewImpl()` 不会被调用(这也是 §3.5 中「不建表」的原因)。反过来说,聚合若漏注某一项(例如 fts/mgt 需要的 DB 或 Redis),模块不会报错,而是在首个请求上 panic——各模块的 `applyDependencies` 都是静默跳过 nil。
|
||
|
||
---
|
||
|
||
## 7. 数据与表
|
||
|
||
### 7.1 表清单(按归属模块)
|
||
|
||
| 模块 | 表 |
|
||
| --- | --- |
|
||
| ads | `ads_item`、`ads_pos`(后者空模型) |
|
||
| cms | `cms_accessory`、`cms_category`、`cms_comment`、`cms_pages`、`cms_post`、`cms_relate_category`、`cms_relate_tags`、`cms_site`、`cms_tags` |
|
||
| feedback | `feedback_accessory`、`feedback_images`、`feedback_item` |
|
||
| fts | `fts_record` |
|
||
| initial | `initial_apps`、`initial_areas`、`initial_config`、`initial_country`、`initial_datas` |
|
||
| logs | `log_data`(**无 `TableName()`**,按 GORM 默认名——推断) |
|
||
| mgt | `mgt_users`、`mgt_roles`、`mgt_departments`、`mgt_applications`(四者无 `TableName()`,用 GORM 复数默认名)、`mgt_permission`、`mgt_link_user_dpt`、`mgt_link_user_role`、`mgt_link_user_pmn`、`mgt_link_user_app`、`mgt_link_role_pmn`、`mgt_link_role_app`、`mgt_link_dpt_role`、`mgt_link_dpt_pmn` |
|
||
| passport | `passport_account`、`passport_data`、`passport_notify`、`passport_provider`(**无 `TableName()`**)、`passport_rights_expiry`、`passport_score`、`passport_statistics`、`passport_tags`、`passport_verify` |
|
||
| sender | `SenderTemplate`(**无 `TableName()`**,按 SDK 的 `SingularTable` 推断为 `sender_template`——推断) |
|
||
| address | `address_library` |
|
||
| mall | `mall_ads`、`mall_apply`、`mall_category`、`mall_freight`、`mall_freight_attr`、`mall_freight_deny`、`mall_notice`、`mall_product`、`mall_product_attr`、`mall_product_comment`、`mall_product_photos`、`mall_product_spec`、`mall_staff`、`mall_store`、`product_category`、`product_spec` |
|
||
| market | `market_agency`、`market_supply` |
|
||
| order | `order_cart`、`order_coupon`、`order_details`、`order_summary` |
|
||
| wallet | `wallet_apply_cash`、`wallet_bank`、`wallet_basic`、`wallet_payment`、`wallet_record`、`wallet_refund` |
|
||
|
||
注意 `mall` 里有两个不带 `mall_` 前缀的表:`product_category`、`product_spec`(`mall_product_link_category.go:20`、`mall_product_link_spec.go:20`)。
|
||
|
||
### 7.2 共享字段约定
|
||
|
||
跨模块关联**不用数字外键,全部用字符串 `identity`**(`D:\work\bsm-sdk\core\types\db.go:20-76`):
|
||
|
||
| 嵌入结构 | 字段 | 说明 |
|
||
| --- | --- | --- |
|
||
| `Std_IDIdentity` | `id`、`identity` | `identity` 为 varchar(36) 唯一索引,24 位 NanoID 或 36 位 ULID |
|
||
| `Std_IICUDS` | `id`、`identity`、`created_at`、`updated_at`、`deleted_at`、`status` | 最常用;`status` 默认 0、-1 禁用、1 正常 |
|
||
| `Std_Passport` | `passport_id`、`passport_identity` | 指向用户;**跨模块引用用户就靠 `passport_identity`** |
|
||
| `Std_Owner` | `owner_id`、`owner_identity` | 资源归属 |
|
||
| `Std_Status` | `status` int64 | 仅状态 |
|
||
|
||
业务侧还有一组约定字段:`store_identity`(店铺)、`product_identity`(商品)、`address_identity`(收货地址)、`order_no`(18 位订单号,`module/ec/order/internal/logic/common/no.go:10`)。改写任何一张表的这些列都会波及其它模块,属于破坏性变更。
|
||
|
||
### 7.3 跨模块表访问矩阵(重要)
|
||
|
||
各模块之间**没有 Go 包依赖**,但有直接跨模块读写表:
|
||
|
||
| 使用方 | 目标表 | 归属 | 位置 |
|
||
| --- | --- | --- | --- |
|
||
| cms | `mall_staff` | mall | `module/base/cms/internal/logic/post/add_comment.go:35` |
|
||
| mall | `market_supply` | market | `module/ec/mall/internal/logic/product/item_detail.go:31` |
|
||
| order | `address_library` | address | `summary/submit.go:68`、`summary/quick_create_by_product.go:79`、`mgt/order_create.go:101` |
|
||
| order | `mall_product` | mall | `summary/submit.go:82`、`:112`,`summary/quick_create_by_product.go:51`、`:53`,`mgt/order_create.go:57` |
|
||
| order | `mall_product_spec` | mall | `summary/submit.go:123`、`:204`、`:209`,`summary/quick_create_by_product.go:68`,`mgt/order_create.go:67`、`:141`,`mgt/order_approve.go:60` |
|
||
| order | `mall_store` | mall | `summary/quick_create_by_product.go:35` |
|
||
| order | `product_spec` | mall | `internal/logic/cart/fetch.go:44-46`(原生 SQL JOIN) |
|
||
| wallet | `order_summary` | order | `module/finance/wallet/internal/models/query.go:104` |
|
||
| wallet | `passport_account` | passport | `module/finance/wallet/internal/models/query.go:24`(**硬编码 `account="demo"`**) |
|
||
|
||
order 里这些查询用 `Table("...")` + 本地投影结构体(`models.Product`、`models.Spec`、`OrderAddress`,见 `module/ec/order/internal/models/query.go:3`、`:16`、`types.go:7`),**没有 gorm 模型、没有编译期约束**:mall 改一个列名,order 只会在运行时报错。
|
||
|
||
反向也要记住:`address`、`mall`(含 `mall_staff`)、`market`、`order`、`wallet` 的表都有外部读者,不能随意改列。新加入的 6 个平台模块只新增了上表这一条跨模块读取(cms → `mall_staff`),彼此的 `ads_*`、`cms_*`、`feedback_*`、`fts_record`、`log_data`、`mgt_*` 表都与其它模块无关。
|
||
|
||
---
|
||
|
||
## 8. 核心业务链路
|
||
|
||
### 8.1 下单主链路(`Summary.Submit`)
|
||
|
||
`module/ec/order/internal/logic/summary/submit.go:24-247`:
|
||
|
||
1. 鉴权:`service.ParseMetaCtx(ctx, &service.ParseOptions{RoleValue: "Mall_Admin"})`;**如果请求带了内嵌 `Address`,角色校验被置空**(`:26-33`)
|
||
2. 读购物车:`order_cart` where `passport_identity = <当前用户>`(`:45`)
|
||
3. 取收货地址:内嵌 `Address` 优先;否则读 `address_library` where `identity = <AddressIdentity>`(`:55-76`)
|
||
4. 按店铺分组:对每个购物车项读 `mall_product` 取 `store_identity`,以 `store_identity` 为 key 分桶(`:79-95`)
|
||
5. 每个店铺生成 `summaryIdentity = utils.UUID()` 与 `orderNo = common.CreateOrderNo()`(`:103-104`)
|
||
6. 店铺内逐件读 `mall_product`(`title/cover_image/cost_price/supply_id/args/gas_types`)与 `mall_product_spec`(`title/serial_number/price`),累加金额(`:112-135`)
|
||
7. 组装 `order_details` 与 `order_summary`(写入**地址快照**、`Std_Passport`、`StoreID`/`StoreIdentity`、`TransPrice`/`TotalPrice`,`Status = 1`)(`:138-180`)
|
||
8. 单事务四步:建 `order_summary` → 批量建 `order_details` → 逐条扣 `mall_product_spec.stock`(带 `stock >= number` 守卫)→ 清空该用户 `order_cart`(`:187-228`)
|
||
9. 返回第一个店铺的 `order_no` 作为 `Identity`,店铺 identity 列表拼在 `Message`(`:236-246`)
|
||
|
||
**已知缺陷**:`StoreId` 只取第一个购物车项的 `store_id`(`:91-93`),却写给所有店铺分组的 `OrderSummary.StoreID`(`:166`)——多店铺下单时除首店外 `StoreID` 记录错误。`LogisticsFee` 恒为 0。该链路**不触发任何支付调用**,订单以 `status=1` 落库后由钱包侧推进。
|
||
|
||
### 8.2 支付链路
|
||
|
||
- 真实支付入口是 `wallet.Payment.ByOrder` / `ByCharge`(`module/finance/wallet/internal/logic/payment/`),按 `PayChannel`(1 微信 / 2 支付宝 / 3 钱包余额)分发,内部复用 `wechat.go` 的 `MiniPaySign`/`AppPaySign`/`NativePaySign` 与 `alipay.go` 的 `TradeWapPay`/`TradeAppPay`,并写 `wallet_payment`。
|
||
- `wallet.Payment.ByOrder` 会通过 `models/query.go:104` **直接读 `order_summary.trans_price`**(`status=1`)核对金额。
|
||
- **`wallet.Alipay/*` 四个 RPC 与 `wallet.Wechat/AppPreOrder`、`NativePreOrder`、`Transfer` 全是 TODO**,函数体只有参数校验 + 裸 `return`(返回 nil reply、nil error)。也就是说渠道能力只能经 `Payment.*` 走通,走这几个 RPC 会静默失败。
|
||
- `Wechat.WxCallback` 只做验签+解密后原样返回,注释 `// Todo: 业务逻辑`,**不落库、不改支付状态**(`module/finance/wallet/internal/logic/wechat/wx_callback.go:38`)。
|
||
- order 侧的 `Summary.SimulatePay` / `SimulateShipments` / `SimulateReceiving` 是**真实现**,但交易号、金额、备注全是硬编码常量(`module/ec/order/internal/logic/summary/simulate_pay.go:25`),仅用于本地联调状态机,不能当支付/物流对接。
|
||
|
||
### 8.3 身份与验证码链路
|
||
|
||
- 用户身份只在 `passport` 维护,其它模块通过 `passport_identity` 引用。
|
||
- **`passport` 并不调用 `sender`**:全仓库没有 `module/base/sender` 被 `passport` 引用的记录,也没有 `sender.Sms`/`sender.Mail` 的调用点。验证码的生成与校验目前只存在于 `sender.Sms` 自身(Redis 键 `/SMS/Code/{phone}`,`module/base/sender/internal/logic/sms/const.go:5`)。
|
||
- 与之对应,`passport` 的验证码类接口**没有真正比对验证码**:`forget.Verify` 只判空 + 查账号状态(`module/base/passport/internal/logic/forget/verify.go:15-26`),`login.Code`、`login.Quick` 同样只判空。`mall.Staff/Login` 的验证码分支也不校验(`module/ec/mall/internal/logic/staff/login.go:45-58`)。
|
||
- ⚠️ `passport` 的密码算法不统一:注册、改密、登录用 bcrypt,而 **`forget.Reset` 用 `utils.Md5`**(`module/base/passport/internal/logic/forget/reset.go:31`)——重置后的密码无法通过登录校验。
|
||
- `wallet` 的钱包按用户懒创建;`models.InitData()` 里有一段按 `account="demo"` 找默认用户并建钱包的 seed,但该函数只在独立入口经 `srv.Use(models.InitData)` 注册(`module/finance/wallet/cmd/main/main.go:47`),**聚合模式不执行**。
|
||
|
||
### 8.4 平台与运营链路(ads / cms / feedback / fts / logs / mgt)
|
||
|
||
这 6 个模块**不参与下单与支付链路**,也**不通过 gRPC 互相调用**(全仓库没有它们的 `Dependencies`/RPC 客户端;`fts`、`logs`、`mgt` 连 `pb` 都不存在)。它们的链路各自独立,只有下面这一处真正的跨模块耦合:
|
||
|
||
| 链路 | 走向 |
|
||
| --- | --- |
|
||
| 后台管理(mgt) | `POST /rest/mgt/login` → `pub.Login` 校验 `mgt_users` 密码并签发 JWT → `/refresh` 续期 → 其余 `/user`、`/app`、`/role`、`/pmn`、`/dpt` 接口经 `JwtAuth + RequireAdmin` 操作 13 张 `mgt_*` 表。**聚合不播种 root 用户**(§9.7),管理员必须另行建好 |
|
||
| 内容(cms) | 文章/页面/分类/标签读写 9 张 `cms_*` 表;**发布评论时会按 JWT 身份去 `mall_staff` 取名**(`post/add_comment.go:35`),取不到就直接返回 `ErrDB`,`add_comment.go:40` 还对结果做无保护断言 → 非 mall 员工身份评论会失败/panic |
|
||
| 反馈(feedback) | `Add` 写 `feedback_item` + 图片,`Remark`/`Modify`/`Delete` 改主表;没有工单流转、通知或与订单/商品的关联字段 |
|
||
| 文件(fts) | `POST /rest/fts/uploader`(模块自己加 JWT)→ 按 `年-月/identity前2位` 落本地 `Local.UploadDir` 或 MinIO → 写 `fts_record` → 返回 `Local.Site` 拼出的 URL;无秒传、无去重、无下载/删除接口 |
|
||
| 日志(logs) | `POST /rest/logs/create` 批量写 `log_data`,`POST /fetch` 与 `/total` 查询统计;模块内三条路由(含写入)都是匿名,但 ecmall 白名单只放行 `/rest/logs/ping` |
|
||
| 广告(ads) | 只读:`ads.Fetch/ByPos` 按 `pos_key` + `status` 查 `ads_item` 并解析成展示项 |
|
||
|
||
结论:把这 6 个模块接进 ecmall,得到的是**同一个进程、同一套鉴权、同一个数据库**,而不是一条新的业务闭环;它们之间的协作目前只存在于数据库层面。
|
||
|
||
---
|
||
|
||
## 9. 子模块速查
|
||
|
||
### 9.1 ads — 广告位内容分发(只读)
|
||
|
||
- **暴露**:`Fetch`(1),唯一方法 `ByPos`;无 `Config`;gRPC + gateway `POST /ads.Fetch/ByPos`
|
||
- **表**:`ads_item`、`ads_pos`(`ads_pos` 除迁移登记外**没有任何查询**,属空模型)
|
||
- **逻辑**:`internal/logic/fetch`(按 `pos_key` + `status` 查启用广告并解析成文本/图片/视频条目)
|
||
- **端口**:独立 dev 配置 gRPC 12216 / gateway 12102,库 `rst_dev`;`ads_prod.yaml` 与 `ads_test.yaml` 与 dev **逐字相同**(prod 仍指向 `rst_dev` + `CHANGE_ME`)
|
||
- **缺口**:
|
||
- `by_pos.go:21` 直接解引用 `impl.DBService`,聚合未注入 DB 时首个请求 panic(推断)
|
||
- 全量 `Find` 无 `Limit`/分页,广告量大时一次性返回
|
||
- 独立入口 `cmd/main/main.go` 从不调用 `service.Expose`,且 `internal/server/new.go:26-30` 从未给 `Server.Mux` 赋值 → 独立进程的 gateway 形同虚设(推断 404)
|
||
- `impl.MemorySerice`、`impl.RedisService` 只写不读(README 宣称的 Redis 缓存不存在);`pb/const.pb.go`(约 3 千行 `base_ads_blocks`)只被 `scripts/api-docgen` 空导入
|
||
- 独立 gateway 端口 12102 与 `initial` 的 gateway 端口相同(推断:两个模块独立部署时冲突)
|
||
- **测试**:无(`test/rpc/rpc.go` 函数体整段被注释)
|
||
|
||
### 9.2 cms — 内容管理(文章/页面/分类/标签)
|
||
|
||
- **暴露**:`Category`(4)、`Pages`(6)、`Post`(19)、`Site`(5)、`Tags`(4),共 5 个服务 38 个方法;无 `Config`;全部走 gRPC + gateway(`POST /cms.<Service>/<Method>`)
|
||
- **表**:9 张 —— `cms_post`、`cms_category`、`cms_tags`、`cms_comment`、`cms_pages`、`cms_site`、`cms_accessory`、`cms_relate_category`、`cms_relate_tags`
|
||
- **逻辑**:`internal/logic/{post,pages,category,tags,site}`;鉴权靠每个 logic 方法内部调 `service.ParseMetaCtx`(**不在聚合层统一处理**)
|
||
- **端口**:gRPC 12410 / gateway 12409;独立 dev 库 `ec_mall`
|
||
- **缺口**:
|
||
- `site` 子域整体空壳却已注册:`site/create.go:20-27`、`modify.go:21-28`、`delete.go:27-32` 返回固定 OK 不写库;`fetch.go:18-22`、`get.go:24-26` 返回零值 + `nil` error
|
||
- **未鉴权写入**:`category/modify.go` 完全不调 `ParseMetaCtx`
|
||
- 跳过鉴权的读:`post/fetch.go`、`post/get_by_key.go`、`post/get_by_identity.go`、`post/search.go`、`pages/{fetch,get_by_key,get_by_identity}.go`、`category/fetch.go`;`post/comment_list.go:24` 的 `ParseMetaCtx` 被注释
|
||
- panic/错算:`post/add_comment.go:40` 未检查的类型断言;`post/search.go:31` 的 `if pageSize < 50` 判断反向、`:43` 先 `make(len(data))` 再 append(结果翻倍且前半为 nil);`post/comment_list.go:18-19` 在钳制前取值
|
||
- 可刷量:`incr_post_like.go:33` 要求 `op_identity` 却从不使用(`models/query.go:346` TODO),重复点赞不去重
|
||
- 静默失败:`models/query.go:36` 忽略 Create 错误、`:379` 忽略 Count 错误、`:375` 用 `Find` 代替 `First`(删除不存在的分类也报成功)
|
||
- `InitData()` 无调用点 → `_RootCategory` 永不创建;生产代码开着 GORM `.Debug()`(`query.go:119` 等)与 `fmt.Println`
|
||
- 跨模块裸读 `mall_staff`(`post/add_comment.go:35`,见 §7.3)
|
||
- **测试**:无(`test/*.http` 的路径已与生成的 `/cms.<Service>/<Method>` 脱节,实测会 404)
|
||
|
||
### 9.3 feedback — 用户反馈工单
|
||
|
||
- **暴露**:`Method`(6):List/Get/Add/Modify/Delete/Remark;无 `Config`;gRPC + gateway `POST /feedback.Method/<Method>`
|
||
- **表**:`feedback_item`、`feedback_images`、`feedback_accessory`
|
||
- **逻辑**:`internal/logic/method`(单包)
|
||
- **端口**:dev 只有单端口 `ListenOn: 0.0.0.0:12210`;dev 库 `milu`
|
||
- **缺口**:
|
||
- **`Get`/`Delete`/`Remark` 完全不调 `ParseMetaCtx`**(`get.go:19-42`、`delete.go:19-35`、`remark.go:19-41`):能读任意工单的 email/phone、删任意工单、改任意备注状态;`Modify` 只解析身份不校验归属(`modify.go:24`)
|
||
- 假成功:`Delete`/`Remark`/`Modify` 不校验影响行数即返回 OK(`delete.go:26-34`、`remark.go:32-40`);`Updates(struct)` 忽略零值 → 无法置 `status=0` 或清空备注
|
||
- `modify.go:36-38` 用新 `utils.UUID()` 覆盖 `identity` 后 `Updates`(推断:unique 键被改写,记录失联);`modify.go:71-75` 构造 `FeedbackAccessory` 未设 `Identity`(第二条起唯一索引冲突)
|
||
- `list.go:50` 过滤列写 `username`,模型列名是 `user_name`(`feedback_item.go:15`)→ 带该条件即 SQL 报错;`list.go:38-45` 传 `agency` 就跳过 `passport_identity` 过滤(且 `agency` 无写入点,筛选恒假)
|
||
- 假级联:`delete.go:25` 注释称会自动删图片/附件,但模型没有 `OnDelete`,实际只软删主表 → 子表残留
|
||
- 独立 yaml 用的是旧键(`Name`/`ListenOn`/`Dsn`),既无 `Service:` 也无 `Databases:` → 独立启动会在 `conf.New` 直接 fatal(推断)
|
||
- **测试**:无(`test/rpc/rpc.go` 全注释)
|
||
|
||
### 9.4 fts — 文件上传与存储
|
||
|
||
- **暴露**:**没有 gRPC**。`ExposeOptions` 只有 `Dependencies` + `Engine`;`Config` 就是 `internal/config.SrvConfig`(含 `MinioOss`、`Local`、`FtsConfig`)
|
||
- **REST**:`GET /rest/fts/ping`、`GET /rest/fts/config`(匿名)、`POST /rest/fts/uploader`(模块自带 `middleware.JwtAuth(true)`)
|
||
- **表**:`fts_record`(只写不读,`handler.go:101`);**无跨模块表访问**
|
||
- **逻辑**:`internal/logic`(上传编排)+ `internal/routers/uploader.go`;存储走本地 `Local.UploadDir` 或 MinIO,本地 URL 由 `Local.Site` + `年-月/identity前2位` 拼成
|
||
- **端口**:dev/test/prod 三份配置完全相同:单端口 16290,dev 库 `bsm_dev`;`Local.Site` 指向外部静态站 `http://files.apinb.com`
|
||
- **缺口**:
|
||
- **路径穿越面**:`bucket` 直接取自 `c.PostForm`(`handler.go:27`)未做任何校验,随后 `filepath.Join(UploadDir, bucket, ...)`(`provider.go:26,37`)并进入返回 URL(`provider.go:71`)——`../` 可逃出 `UploadDir`
|
||
- 上传校验薄弱:只按**文件名后缀**白名单且区分大小写(`handler.go:63-64,127-133`),无 MIME/魔数校验;大小检查发生在 multipart 完全解析之后(`handler.go:49-57`)
|
||
- `provider.go:115` 用 `identity[0:2]`,`claims.Identity` 短于 2 字节即切片越界(被 `gin.Recovery` 兜成 500)
|
||
- nil 解引用:`FtsConfig`/`Local`/`MinioOss` 三者都不校验(`config.go:40` 只查 `Service`、`Cache`),缺失时 `handler.go:49,57,128`、`provider.go:26,71,79` panic
|
||
- `record.Status` 硬编码 0、`HandleCmd/HandleArgs` 永不写入、算好的 `Hash` 没有秒传/去重实现;文件已落盘后才因 chmod 失败返回错误(`provider.go:63-67`)
|
||
- 死代码:`internal/logic/fetch.go` 整段注释、`models/query.go:4-5` 空 `InitData()`、`internal/errors` 与 `internal/response`(262 行)无导入方
|
||
- `Local.Site`/`UploadDir` 与匿名 `GET /rest/fts/config` 会把上传配置直接暴露给未认证调用方
|
||
- **测试**:`internal/routers/register_test.go`(**当前失败**,断言 `/rest/fts/v1/*` 而实现是 `/rest/fts/*`;文件是 CRLF,也被 `gofmt -l` 判为未格式化);`test/fts_test.go` 带 `//go:build integration` 且 URL 指向从未注册的 `/fts/Transfer/*`
|
||
|
||
### 9.5 initial — 客户端初始化与基础字典
|
||
|
||
- **暴露**:`Check`(Hello/Config/Updates)、`Data`(Country/Areas/Datas);`ExposeOptions` **无 `Config`**
|
||
- **表**:`initial_apps`、`initial_areas`、`initial_config`、`initial_country`、`initial_datas`
|
||
- **逻辑**:`internal/logic/check`(配置与版本)、`internal/logic/data`(字典),各带 Redis 缓存封装(键 `config`、`country`、`areas`、`datas`)
|
||
- **端口**:gRPC 12101 / gateway 12102
|
||
- **缺口**:`data/areas_cache.go:33-34` 按 `enabled = ?`、`show_town = ?` 过滤并按 `sort_order` 排序,而 `models.InitialAreas` 只有 `id/country_id/country_code/pid/deep/name/pinyin_prefix/pinyin/ext_id/ext_name` 十列,**这三个列在模型里都不存在**;`check/config_cache.go:21` 把入参 `os` 直接拼进 `ORDER BY`(注入面);`check/updates.go:39` 用字符串不等判断版本,非语义化版本比较;注入的 `MemorySerice` 无人使用
|
||
- **测试**:无
|
||
|
||
### 9.6 logs — 操作日志采集与查询
|
||
|
||
- **暴露**:**没有 gRPC**,也**没有 `Config` 类型**(模块级配置只在 `internal/config`,聚合无法注入)。`ExposeOptions` 只有 `Dependencies` + `Engine`
|
||
- **REST**:`GET /rest/logs/ping`、`POST /rest/logs/create|fetch|total`,模块内**全部注册为匿名**(`internal/routers/register.go:23` 的 JWT 中间件被注释,`middleware`/`impl` 的 import 也已不存在)。注意聚合层只把 `/rest/logs/ping` 列入白名单,`create`/`fetch`/`total` 在 ecmall 下仍需 JWT(见 §4.5)
|
||
- **表**:`log_data`(模型 `LogData` 没有 `TableName()`,按 GORM 默认名——推断);无跨模块表访问
|
||
- **逻辑**:`internal/logic/log`(批量写入 + 查询/统计)、`internal/logic/hello`(ping)
|
||
- **端口**:dev 配置 `Service: oplogs`、单端口 16289、`Driver: dm`(**达梦**,`database=DAMENG`);`logs_test.yaml` 换成 postgres 的 `rst_dev`,prod 与 dev 端口一致
|
||
- **缺口**:
|
||
- 分页参数**从不读取**(`fetch.go:33-34,48-53`)→ 永恒 page=1/size=50,客户端翻页静默失效
|
||
- `fetch.go:41-42` 条件键写 `op_ip` 却读 `request["ip"]`,缺 `ip` 时对 nil 断言 panic;`fetch.go:36,39,45` 是无保护类型断言(JSON 数字解成 float64)→ panic
|
||
- `total.go:28` 用 `Group + First`,只返回一个分组行而不是各级别统计;空表返回 ErrDB 而不是零值
|
||
- **`create.go:21` 的 IP 判定与其注释(`create.go:19`)相反**:拦住 `127./localhost/10./172./192.`,内网服务写不进来、公网 IP 反而通过
|
||
- `create.go:28-40` 无批量上限,`create`/`fetch`/`total` 均未鉴权;`log_data.go:19-20` 的 `Hmac`/`Encry` 从不使用或校验
|
||
- `cmd/cli/main.go` 是死代码(路径过期、`HttpPost` 错误被忽略);`fetch.go:36,39`、`total.go:24` 的 `tx.Where` 未回写(GORM 首次链式后原地改动,能用但脆弱——推断)
|
||
- **测试**:无(`test/*.http` 仍指向 `/oplogs/v1/*`)
|
||
|
||
### 9.7 mgt — 后台管理(RBAC 底座)
|
||
|
||
- **暴露**:**没有 gRPC**;`ExposeOptions` 只有 `Dependencies` + `Engine`;`Config` 为 `Mgt` 节(`InitRootUser`、`SecretKey`)
|
||
- **REST**:`/rest/mgt` 下共 63 条路由
|
||
- 匿名 5 条:`GET /ping`、`GET /session`、`POST /login`、`POST /refresh`、`POST /reset`(`internal/routers/register.go:31-36`)
|
||
- `/user` 15 条:4 条本人可查(`role`/`app`/`pmn`/`pmn_tree`,仅需 JWT)+ 11 条 admin(create/del/detail/modify/fetch/list/set_role/del_role/set_pmn/modify_pmn/del_pmn)
|
||
- `/app` 8、`/role` 12、`/pmn` 8、`/dpt` 15,全部 `JWT + admin`
|
||
- 中间件:SDK `middleware.JwtAuth(true)` + 模块自己的 `internal/middleware/rbac.go`(`RequireAdmin:39`、`IsSuperAdmin:19`、`EnsureSelfOrAdmin:57`)
|
||
- **表**:13 张 —— `mgt_users`、`mgt_roles`、`mgt_departments`、`mgt_applications`(四者无 `TableName()`,用 GORM 复数默认名)、`mgt_permission`,以及 8 张 `mgt_link_*`(user_dpt、user_role、user_pmn、user_app、role_pmn、role_app、dpt_role、dpt_pmn);无跨模块表访问
|
||
- **端口**:dev 单端口 18001、库 `ec_dev`(127.0.0.1:19432)、Redis `19379/0`;prod 库 `factor_prod`
|
||
- **缺口**:
|
||
- **聚合不建表也不播种 root**:`AutoMigrate` 与 `InitRootUserData()` 只在 `models.New` 里执行,而它挂在 `impl.NewImpl()` ← `cmd/main` 上;聚合只调 `service.Expose`(见 §3.5)。`InitRootUser` 的注释/README 说「未配置默认 true」,实现是 `nil → false`(`internal/config/config.go:19-27`),且 `mgt_prod.yaml`/`mgt_test.yaml` 都没有该键 → 生产默认关闭
|
||
- **匿名写端点**:`POST /rest/mgt/reset` 无鉴权即可改密(仅靠 Redis 短信码,无频率限制);`GET /rest/mgt/session` 匿名写 session(聚合的 `service.Expose` 已注册 `mysession` store,`internal/service/service.go:35`,所以这条能跑通)
|
||
- `user/list.go:29` 的原生 SQL 写的是 `mgt_application`,真实表名是 `mgt_applications` → 该接口在 Postgres 上必然失败
|
||
- `user/set_pmn.go:47,58` 在循环里赋值 `Appdta`、却在循环外无条件 `Create` → 列表为空时插入 `(app_id=0, user_id=0)` 的关联行
|
||
- `IsSuperAdmin` 用 `account == "root"` 或硬编码中文角色名「超级管理员」判定(`rbac.go:14-15,24,31`),DB 出错一律返回 false
|
||
- nil 解引用:`impl.RedisService`/`impl.DBService` 未被注入时 `pub/forget.go:58`、`rbac.go:21` panic;`SecretKey` 字段没有 yaml tag,实际值来自 `BSM_JwtSecretKey`(`internal/config/config.go:39`)
|
||
- 死代码:`builtinManagementMenuTemplates()`、`ForgetPwdByPwd`(空函数且未注册)、`SaveToken` 均无调用点
|
||
- **测试**:`internal/routers/register_test.go`(本机通过)
|
||
|
||
### 9.8 passport — 用户账号与身份认证
|
||
|
||
- **暴露**:`Account`(Get/SetData/SetPassword/TagCreate/TagRemove/Statistics)、`Forget`(Verify/Reset)、`Login`(Pwd/Code/Quick)、`Register`(Pwd/Code)、`Verify`(Request/JumioCallback);**有 `Config`**(`Passport` 节:`Token`、`Kyc`、`WeChatConf`)
|
||
- **表**:9 张 `passport_*`
|
||
- **逻辑**:`account`、`common`(JWT 生成)、`forget`、`login`、`register`、`verify`(Jumio KYC)
|
||
- **端口**:gRPC 12426 / gateway 12425;独立 dev 库为 `ec_dev:19432`、Redis `19379`
|
||
- **缺口**:
|
||
- `verify.JumioCallback` 是占位:不验签、不落库,只打日志后返回成功(`internal/logic/verify/jumio_callback.go:14-35`)
|
||
- `forget.Reset` 用 MD5,与登录/改密的 bcrypt 不一致(`forget/reset.go:31`)
|
||
- `forget.Verify`、`login.Code`、`login.Quick` 不比对验证码
|
||
- `account.Statistics` 的 `login_count` 统计的是账号行数(最多 1),不是登录次数
|
||
- `models/cache.go` 整套缓存/验证码/登录尝试 API 与 `query.CreateAccount` **无任何调用方**(死代码)
|
||
- `WeChatConf` 只有 json tag 没有 yaml tag,`config.Spec.WeChat` 在代码中未被使用
|
||
- **测试**:无(仅 `test/grpc/main.go` 手工客户端,硬编码 `api.apinb.com:10020`)
|
||
|
||
### 9.9 sender — 短信与邮件
|
||
|
||
- **暴露**:`Mail`(Send)、`Sms`(Send/Verify);**有 `Config`**(`SMTP` map、`SMS` map、`Code`)
|
||
- **表**:`SenderTemplate`(无 `TableName()`)
|
||
- **逻辑**:`internal/logic/sms`(验证码生成/SetNX 落 Redis/校验、渠道分发)、`internal/logic/mail`(模板查询 + `html/template` + SMTP)
|
||
- **端口**:gRPC 12208 / gateway 12207;独立 dev 库为 `bsm_dev`
|
||
- **缺口**:
|
||
- **腾讯云短信是空实现且静默成功**:`TencentSender` 直接 `return nil, nil`(`internal/logic/sms/send.go:144-146`),调用方随后 `json.Marshal(nil)` 得到 `"null"` 当成功返回
|
||
- 邮件只实现了 `qq` 渠道,其它 provider 返回 `ErrNotProvider`(`internal/logic/mail/send.go:55-60`);`impl.NewSMTP` 直接返回 nil,`Provider.Google`/`Provider.QQ` 是死字段
|
||
- 日发送量限制失效:`limitKey` 只 `Get` 从不写回(`send.go:42-51`),计数永不增长
|
||
- 黑名单只读不写:仓库内没有写 `/SMS/BlackList/` 的代码;配置项 `Code.BlackListFilter`、`Code.GenerateCode`、`Code.CokeyKey` 均无读取点
|
||
- `sms/verify.go:34-35` 在**比对失败**分支删键,成功反而保留验证码
|
||
- `config.Spec.Code` 无 nil 校验即解引用(`send.go:49` 等)
|
||
- **测试**:仅两个 `//go:build integration` 的联网测试(`test/grpc/mail_test.go`、`sms_test.go`),默认 `go test` 不执行
|
||
|
||
### 9.10 address — 收货地址簿
|
||
|
||
- **暴露**:`Library`(Create/Modify/Get/Fetch/Delete);无 `Config`
|
||
- **表**:`address_library`
|
||
- **逻辑**:`internal/logic/library`(唯一子包)
|
||
- **端口**:gRPC 12460 / gateway 12459;独立 dev 库为 `ec_mall`
|
||
- **注意**:`etc/address_dev.yaml` 的 `Service:` 写成了 `order`(应为 `address`),配置校验只要求非空所以没暴露问题
|
||
- **缺口**:`fetch.go:38` 有模板遗留 TODO(功能已实现);无其它空实现
|
||
- **测试**:无
|
||
|
||
### 9.11 mall — 店铺与商品
|
||
|
||
- **暴露**:`Ads`(5)、`Category`(4)、`Freight`(9)、`Notice`(4)、`Product`(21)、`Staff`(7)、`Store`(10);无 `Config`
|
||
- **表**:16 张(见 §7.1)
|
||
- **逻辑**:`internal/logic/{ads,category,freight,notice,product,staff,store}` + `internal/password`(bcrypt)+ `internal/excode`
|
||
- **端口**:gRPC 12420 / gateway 12419;独立 dev 库为 `ec_mall`
|
||
- **缺口**(多为「返回成功但不做事」):
|
||
- `freight` 下 `create`、`modify`、`delete`、`deny_region_create`、`deny_region_delete`、`deny_region_modify`、`deny_region_remove` 全部返回固定 `OK` 不写库;`detail`、`fetch`、`deny_region_fetch` 是裸 `return`(`nil, nil`)
|
||
- `store/apply_join`、`store/mini_code` 空实现;`store/licensing` 只查店铺后返回 OK
|
||
- `product/item_detail_by_spec.go:16-18` 是 `// TODO: valid code` + 裸 `return`
|
||
- 孤儿函数:`freight/remove.go` 与 `freight/deny_region_remove.go` 有真实实现但**没有被接线**,server 映射到的是空实现版本
|
||
- `staff/login.go:45-58` 的验证码分支不校验验证码即签发 token;非 1/2 的 `LoginGenre` 返回 `ErrUnimplemented`
|
||
- `item_fetch.go:28` 声明并用于 SQL 的 `idList` 从未 append;`store/search.go:39` 用 `keyword` 过滤但列名是 `keywords`
|
||
- `product/item_detail.go:31` 跨模块读 `market_supply`
|
||
- **测试**:仅 `internal/password/password_test.go`
|
||
|
||
### 9.12 market — 代理与供应商
|
||
|
||
- **暴露**:`Agency`(9)、`Supply`(5)、`Data`(5);无 `Config`
|
||
- **表**:`market_agency`、`market_supply`
|
||
- **逻辑**:`internal/logic/agency`(省代账号全生命周期,含审核)、`internal/logic/supply`、`internal/logic/data`(看板)
|
||
- **端口**:gRPC 12424 / gateway 12423;独立 dev 库为 `ec_mall`
|
||
- **缺口**:
|
||
- `logic/data` 下 5 个方法(Overview/MemberFetch/MemberDetails/OrderFetch/OrderDetails)**全部只有 TODO + 裸 `return`**,返回 `(nil, nil)`
|
||
- `agency/fetch.go:35`、`agency/pending.go:36` 写成 `tx.Where(...)` 未回写 `tx`,identity 过滤很可能不生效
|
||
- `agency/modify.go:53`、`supply/modify.go:46` 返回的 `Identity` 从未赋值,恒为空串
|
||
- `agency/create.go:20-23`、`delete.go:17-21`、`supply/create.go:19-23` 的鉴权被注释掉;`agency/create.go:50,52` 残留 `fmt.Println` 调试输出(含密码哈希)
|
||
- prod/test 配置的匿名方法是 `mall.ping.hello`(与 dev 的 `market.ping.hello` 不一致)
|
||
- **测试**:仅 `internal/password/password_test.go`
|
||
|
||
### 9.13 order — 购物车与订单
|
||
|
||
- **暴露**:`Cart`(4)、`Coupon`(1)、`Mgt`(7)、`Summary`(10);无 `Config`
|
||
- **表**:`order_cart`、`order_coupon`、`order_details`、`order_summary`
|
||
- **逻辑**:`internal/logic/{cart,common,coupon,mgt,summary}`
|
||
- **端口**:gRPC 12442 / gateway 12441;独立 dev 库为 `ec_mall`
|
||
- **缺口**:
|
||
- `mgt/order_modify.go:25-33` 是**假成功**:只有 TODO 注释,直接返回 `Code:0, Message:"OK"`,不写库
|
||
- 下单主链路缺陷见 §8.1(多店铺 `StoreID` 错误)
|
||
- `summary/quick_create_by_product.go` 大量对 `map[string]any` 的无保护类型断言(`:43`、`:62`、`:102`、`:104`、`:112-117`…),NULL 或字段缺失会 panic;配送员查询整段被注释
|
||
- `cart/fetch.go:40-48` 原生 SQL 出错时静默丢数据(只在 `err == nil` 时追加结果,不记录也不返回)
|
||
- `coupon/by_status.go:52` 把 `Intro` 赋给 `Amount`;入参 `Status` 未被使用
|
||
- 死代码:`models/query.go:22 QuicklyCreateOrder`、`order_summary.go:70 GetSummaryCnt`、`:83 GetSummaryList`(后者引用了模型中不存在的列 `buyer_identity`/`merchant_identity`/`pay_status` 等)
|
||
- `Simulate*` 见 §8.2
|
||
- **测试**:`test/rpc/` 下 4 个联网集成测试(`cart_test.go`、`coupon_test.go`、`mgt_test.go`、`summary_test.go`)都带 `//go:build integration`,默认 `go test` 不编译不执行;同目录 `bisic_test.go` **没有 build tag,但也不含任何 `Test*` 函数**(只定义 `GetCoon()` 辅助函数,内硬编码 `127.0.0.1:12442` 与 `BSM_ORDER_TOKEN`),所以默认 `go test ./...` 对这个包只是编译通过、不跑任何用例
|
||
|
||
### 9.14 wallet — 钱包与支付
|
||
|
||
- **暴露**:`Alipay`(4)、`Basic`(8)、`Payment`(6)、`Wechat`(5);**有 `Config`**(`WeChat`、`Alipay`、`Wallet`、`QrCodeSavePath`)
|
||
- **表**:6 张 `wallet_*`
|
||
- **逻辑**:`internal/logic/basic`(钱包、支付密码、银行卡、提现)、`internal/logic/payment`(渠道编排)、`internal/logic/wechat`、`internal/logic/alipay`
|
||
- **端口**:gRPC 12238 / gateway 12239
|
||
- **缺口**:
|
||
- `Alipay.WapPay/PagePay/AppPay/Transfer` 与 `Wechat.AppPreOrder/NativePreOrder/Transfer` **全是 TODO**(裸 `return`);只有 `Wechat.JsapiPreOrder` 真正实现
|
||
- `Wechat.WxCallback` 不落库不改状态
|
||
- `jsapi_pre_order.go:22`、`wx_callback.go:26` 丢弃 `NewWechat()` 的错误,失败时会 nil 解引用
|
||
- `wechat.go:48` 把 `WeChat.PrivateKey` 配置值当**文件路径**读取
|
||
- `config.Spec.Alipay`/`Wallet` 在 `etc/wallet_dev.yaml` 中**根本没有这两个节**,指针为 nil,相关分支依赖调用时机
|
||
- `wallet_test.yaml`/`wallet_prod.yaml` 用的是旧键名(`Name`/`ListenOn`/`Dsn`),与当前 `SrvConfig` 不匹配
|
||
- `WalletRefund` 只有模型定义无任何读写;`ChargeWallet` 无调用方
|
||
- 跨模块读 `order_summary`、`passport_account`(硬编码 `account="demo"`,见 §7.3)
|
||
- **测试**:无
|
||
|
||
---
|
||
|
||
## 10. 缺口与风险汇总(按优先级)
|
||
|
||
**P0 — 影响正确性/安全**
|
||
|
||
| 问题 | 位置 |
|
||
| --- | --- |
|
||
| `forget.Reset` 用 MD5,与登录 bcrypt 不一致,重置后无法登录 | `module/base/passport/internal/logic/forget/reset.go:31` |
|
||
| 验证码接口不比对验证码(passport 三处 + mall staff 登录) | `passport/internal/logic/forget/verify.go:15-26`、`login/code.go`、`login/quick.go`、`mall/internal/logic/staff/login.go:45-58` |
|
||
| `JumioCallback` 不验签不落库即返回成功 | `module/base/passport/internal/logic/verify/jumio_callback.go:14-35` |
|
||
| `WxCallback` 不落库不改支付状态 | `module/finance/wallet/internal/logic/wechat/wx_callback.go:38` |
|
||
| 聚合启动不建表不 seed(部署必踩) | §3.5 |
|
||
| 聚合把所有模块摁到同一个库,与各自独立 dev 库不同 | §3.5 |
|
||
| market `agency/create`、`delete`、`supply/create` 鉴权被注释 | `module/ec/market/internal/logic/agency/create.go:20-23` 等 |
|
||
| initial `config_cache` 把入参拼进 `ORDER BY`(注入面) | `module/base/initial/internal/logic/check/config_cache.go:21` |
|
||
| feedback `Get`/`Delete`/`Remark` 无鉴权且无归属校验(任意读 email/phone、删单、改备注) | `module/base/feedback/internal/logic/method/{get,delete,remark}.go` |
|
||
| mgt `POST /rest/mgt/reset` 匿名即可改密(仅靠短信码,无频率限制) | `module/base/mgt/internal/routers/register.go:36` |
|
||
| logs `create`/`fetch`/`total` 全部匿名,`create` 还无批量上限 | `module/base/logs/internal/routers/register.go:23-27`、`internal/logic/log/create.go:28-40` |
|
||
| logs 写入接口的内网 IP 拦截与注释相反:内网写不进、公网放行 | `module/base/logs/internal/logic/log/create.go:19-21` |
|
||
| fts 上传的 `bucket` 直接取自表单且不校验 → `../` 可逃出 `UploadDir` | `module/base/fts/internal/logic/provider.go:26,37`(来源 `internal/routers/handler.go:27`) |
|
||
| cms `Category.Modify` 无鉴权写入;多处读接口跳过 `ParseMetaCtx` | `module/base/cms/internal/logic/category/modify.go`、`post/{fetch,search}.go` 等 |
|
||
| mgt `InitRootUser` 注释与实现相反(`nil → false`),prod 配置缺该键 → 管理员不播种 | `module/base/mgt/internal/config/config.go:19-27` |
|
||
|
||
**P1 — 功能缺失但「看起来成功」**
|
||
|
||
| 问题 | 位置 |
|
||
| --- | --- |
|
||
| mall freight 7 个写操作返回固定 OK 不写库;3 个查询裸 `return` | `module/ec/mall/internal/logic/freight/*` |
|
||
| mall `store/apply_join`、`mini_code` 空实现 | `module/ec/mall/internal/logic/store/` |
|
||
| mall `product/item_detail_by_spec` 空实现;实现好的两个 delete 未接线 | `module/ec/mall/internal/logic/product/item_detail_by_spec.go:16-18`、`freight/remove.go`、`freight/deny_region_remove.go` |
|
||
| market `logic/data` 5 个方法全空 | `module/ec/market/internal/logic/data/*` |
|
||
| order `mgt/order_modify` 假成功 | `module/ec/order/internal/logic/mgt/order_modify.go:25-33` |
|
||
| wallet Alipay 全部 4 个 + Wechat 3 个 RPC 为 TODO | `module/finance/wallet/internal/logic/{alipay,wechat}/` |
|
||
| sender 腾讯云短信空实现且静默成功;邮件仅 qq | `module/base/sender/internal/logic/sms/send.go:144-146`、`mail/send.go:55-60` |
|
||
| cms `site` 5 个方法空壳(3 个假成功 + 2 个返回零值) | `module/base/cms/internal/logic/site/*.go` |
|
||
| feedback `Delete`/`Remark`/`Modify` 不校验影响行数即返回 OK;`Updates(struct)` 忽略零值 | `module/base/feedback/internal/logic/method/{delete,remark,modify}.go` |
|
||
| mgt `user/list` 原生 SQL 表名写成 `mgt_application`(真实 `mgt_applications`)→ 接口必然失败 | `module/base/mgt/internal/logic/user/list.go:29` |
|
||
| mgt `user/set_pmn` 列表为空时仍插入 `(app_id=0,user_id=0)` 关联行 | `module/base/mgt/internal/logic/user/set_pmn.go:47,58` |
|
||
| ads `AdsPos` 空模型;`ByPos` 全量 `Find` 无分页 | `module/base/ads/internal/logic/fetch/by_pos.go:21` |
|
||
| fts `record.Status` 恒 0、`HandleCmd/Args` 永不写入、`Hash` 没有秒传/去重 | `module/base/fts/internal/logic/handler.go:70,85` |
|
||
|
||
**P2 — 静默失效/数据不一致**
|
||
|
||
| 问题 | 位置 |
|
||
| --- | --- |
|
||
| sender 日发送量限制永不生效;黑名单只读不写;验证码失败时才删键 | `module/base/sender/internal/logic/sms/send.go:42-51`、`verify.go:34-35` |
|
||
| order 多店铺下单 `StoreID` 记录错误 | `module/ec/order/internal/logic/summary/submit.go:91-93`、`:166` |
|
||
| market `agency/fetch`、`pending` 的 identity 过滤未生效 | `module/ec/market/internal/logic/agency/fetch.go:35`、`pending.go:36` |
|
||
| market `modify` 返回的 Identity 恒为空 | `module/ec/market/internal/logic/agency/modify.go:53`、`supply/modify.go:46` |
|
||
| order `coupon/by_status` 把 `Intro` 当 `Amount` | `module/ec/order/internal/logic/coupon/by_status.go:52` |
|
||
| mall `item_fetch` 的 `idList` 从未赋值;`store/search` 列名不符 | `module/ec/mall/internal/logic/product/item_fetch.go:28`、`store/search.go:39` |
|
||
| initial 地区缓存查询不存在的列 | `module/base/initial/internal/logic/data/areas_cache.go:33` |
|
||
| `Dependencies.Cache` 在 address/market/order 无效、在 logs 缺失、在 ads/cms/feedback/fts/mgt 无读取点 | `module/{base,ec}/*/service/dependencies.go` |
|
||
| `address_dev.yaml` 的 `Service` 写成 `order` | `module/ec/address/etc/address_dev.yaml:1` |
|
||
| order `quick_create_by_product` 无保护类型断言易 panic | `module/ec/order/internal/logic/summary/quick_create_by_product.go:43,62,102,104` |
|
||
| logs 分页参数从不读取(恒 page=1/size=50) | `module/base/logs/internal/logic/log/fetch.go:33-34,48-53` |
|
||
| logs `op_ip` 条件读 `request["ip"]`,且多处无保护类型断言 → panic | `module/base/logs/internal/logic/log/fetch.go:36,39,41-45` |
|
||
| logs `total` 用 `Group + First`,只返回一个分组行(空表返回 ErrDB) | `module/base/logs/internal/logic/log/total.go:28` |
|
||
| feedback `list` 过滤列写 `username`(列名是 `user_name`);`agency` 筛选恒假且可越权列表 | `module/base/feedback/internal/logic/method/list.go:38-50` |
|
||
| feedback `Modify` 覆盖 `identity`;`FeedbackAccessory` 未设 `Identity` | `module/base/feedback/internal/logic/method/modify.go:36-38,71-75` |
|
||
| cms `post/search` 分页判断反向且结果翻倍;`comment_list` 钳制失效 | `module/base/cms/internal/logic/post/search.go:31,43`、`comment_list.go:18-19` |
|
||
| cms 点赞可刷量(`op_identity` 要求却从不使用) | `module/base/cms/internal/logic/post/incr_post_like.go:33` |
|
||
| ads DB 未注入即 panic;fts `identity[0:2]` 越界、`Local`/`MinioOss`/`FtsConfig` 不校验 | `module/base/ads/internal/logic/fetch/by_pos.go:21`、`module/base/fts/internal/logic/provider.go:115` |
|
||
| mgt `IsSuperAdmin` 硬编码 `root` 与中文角色名「超级管理员」 | `module/base/mgt/internal/middleware/rbac.go:14-15,24,31` |
|
||
|
||
**P3 — 死代码/噪音**(不影响运行,但改动时别被误导)
|
||
|
||
- passport `models/cache.go` 整套缓存 API、`query.CreateAccount` 无调用方
|
||
- wallet `WalletRefund` 无读写、`ChargeWallet` 无调用方
|
||
- order `QuicklyCreateOrder`、`GetSummaryCnt`、`GetSummaryList` 无调用方
|
||
- mall `InitData()`(默认店铺 + root 员工 seed)无调用点
|
||
- 各模块 `cmd/cli/main.go` 多为 Hello World;`passport`、`sender`、`initial` 的 README 描述的 Makefile/Dockerfile 在仓库中不存在
|
||
- `config.Spec.Code`、`Code.BlackListFilter`、`Code.GenerateCode`、`Code.CokeyKey` 无读取点
|
||
- 大量 `// TODO: add your logic code & delete this line.` 只是模板残留,功能已实现(例如 `address/internal/logic/library/fetch.go:38`)
|
||
- ads `pb/const.pb.go`(约 3 千行 `base_ads_blocks`)只被 `scripts/api-docgen` 空导入;`ads_pos` 除迁移登记外无任何查询
|
||
- cms `InitData()` 无调用点(`_RootCategory` 永不创建);`internal/models/query.go:18-20` 的表名常量值写成 `cms_relate`
|
||
- feedback / logs / ads 的 `cmd/cli/main.go` 与 `test/rpc/rpc.go` 是 Hello World 或整段注释
|
||
- fts `internal/errors`、`internal/response`(262 行)无导入方,`internal/logic/fetch.go` 整段注释,`models/query.go:4-5` 是空 `InitData()`
|
||
- mgt `builtinManagementMenuTemplates()`、`ForgetPwdByPwd`、`SaveToken`、`models/query.go` 均无调用点
|
||
- ads / cms / feedback / fts / logs / mgt 的 README 描述的 Makefile、Dockerfile、swagger、`/health`、`/metrics` 在仓库中都不存在
|
||
|
||
---
|
||
|
||
## 11. 开发指引
|
||
|
||
### 11.1 本地运行
|
||
|
||
```bash
|
||
# 聚合(14 个服务全开)
|
||
cd pkgs/ecmall && go run ./cmd/main --workspace=default
|
||
|
||
# 只跑其中几个服务(REST-only 的 fts/logs/mgt 同样用这里的名字)
|
||
BSM_SERVICES=passport,address,mall,order go run ./cmd/main --workspace=default
|
||
BSM_SERVICES=mgt,logs go run ./cmd/main --workspace=default
|
||
|
||
# 单个子模块独立运行(会走该模块自己的库与端口)
|
||
cd module/ec/order && go run ./cmd/main
|
||
```
|
||
|
||
前置条件:`D:\work\bsm-sdk\core` 必须存在(`go.work` 与各 `go.mod` 都按相对路径 replace 到它),且目标数据库/Redis/etcd 可达。`config.New` 与 `impl.NewImpl` 在依赖缺失时会 **panic**,不是降级启动。
|
||
|
||
### 11.2 验证
|
||
|
||
```bash
|
||
go work sync
|
||
./scripts/verify-workspace.ps1 # Windows:gofmt + 每模块 go vet + go test
|
||
./scripts/verify-workspace.sh # Linux/macOS
|
||
git diff --check
|
||
```
|
||
|
||
已知会使验证脚本失败的历史问题:`module/base/fts/internal/routers/register_test.go` 在工作区是 CRLF,会被 gofmt 判为未格式化;同目录的 `TestRoutesUseRESTModulePrefix` 断言 `/rest/fts/v1/*` 而代码注册的是 `/rest/fts/*`(既有失败,与 ecmall 无关)。
|
||
|
||
### 11.3 增删一个服务
|
||
|
||
1. `pkgs/ecmall/internal/service/` 增加/删除对应的 `xxx.go`(照抄现有文件的 `ExposeOptions` 形态:gRPC 型传 `GRPC`+`Gateway`,REST 型传 `Engine: srv.HTTP`;注意该模块是否需要 `Config`)
|
||
2. 同步 `internal/service/service.go:14-32` 的 `services` 表
|
||
3. 同步 `internal/config/`:需要 `Config` 的模块要加结构体字段 + `assignSharedConfig` 分支(注意 fts / mgt 的 `Config` 不含 `MicroService`)
|
||
4. 同步 `internal/config/config_test.go` 的 `ecmallServices` 与必需配置节断言
|
||
5. 同步 `etc/default_dev.yaml` 的 `Services` 列表(以及新配置节、必要的匿名白名单)
|
||
6. 同步 `internal/service/service_test.go` 的前缀/路由期望值
|
||
7. `go.mod` 加/删 `replace` + `require`,然后 `GOWORK=off go mod tidy`
|
||
8. `go build ./... && go test ./...`
|
||
|
||
`internal/config/config_test.go` 会强制「YAML 服务列表 == 注册表期望值」,`internal/service/service_test.go` 会强制「注册表 → 真实 gRPC/REST 面」,漏改会直接测试失败。
|
||
|
||
### 11.4 新增一个 RPC
|
||
|
||
标准路径:改 `module/<域>/<模块>/proto/*.proto` → 用 `scripts/generate-protobuf.sh` 重新生成 `pb/` → 在 `internal/server/<svc>_server.go` 加方法(委托到 `internal/logic/<子包>`)→ 在 `internal/logic/<子包>/` 加实现。网关路径会由生成的 `*.pb.gw.go` 自动出现为 `POST /{package}.{Service}/{Method}`,动态 RPC 路径自动可用,**无需改 `pkgs/ecmall`**。
|
||
|
||
### 11.5 新增配置段 / 匿名白名单
|
||
|
||
- 专项配置:加在模块 `internal/config` 的结构体(要有 yaml tag),聚合侧在 `SrvConfig` 加同名字段并在 `assignSharedConfig` 回填共享项,然后写进 `pkgs/ecmall/etc/default_dev.yaml`;fts / mgt 只回填 `Base/Databases/Rpc/APM/Etcd`
|
||
- 匿名白名单:写在 `Authorization.Anonymous`。gRPC 方法**用网关形式** `/{package}.{Service}/{Method}`(动态 RPC 的 `/rpc/...` 会被自动规范化后匹配);**REST 路由必须写真实路径**(如 `/rest/logs/ping`),没有等价的网关写法
|
||
- 新增 REST-only 模块时,`ExposeOptions.Engine` 必须传聚合的 `srv.HTTP`,否则路由不会挂到 ecmall 上
|
||
|
||
### 11.6 不要做的事
|
||
|
||
- **不要在 `pkgs/ecmall` 里写业务逻辑**——它是注册表 + 基础设施,业务改动属于 `module/`
|
||
- **不要新增跨模块的表直读**——现有耦合(§7.3)是历史包袱;新交互应走 gRPC
|
||
- **不要改 `identity` / `store_identity` / `product_identity` / `passport_identity` / `order_no` 这些共享列的语义或命名**,它们被多个模块以裸表名依赖
|
||
- **不要假设表会自动建**——见 §3.5
|
||
- **不要假设 `Cache` 注入生效**(address/market/order/logs 以及所有只写不读的模块)
|
||
- **不要以为 `/rest/*` 默认匿名**——聚合的 HTTP 中间件覆盖全部路径,只有白名单里的真实路径才跳过鉴权(§4.5)
|
||
- **不要把 TODO 占位当成已实现能力**——本仓库有大量「返回成功但什么都没做」的接口(§10)
|
||
|
||
---
|
||
|
||
## 12. 相关文档
|
||
|
||
- [根 README](../../README.md) — 全部模块职责与实现边界
|
||
- [系统架构](../../wiki/architecture.md) — 两个聚合入口的注册表差异、请求分流、依赖注入
|
||
- [开发与运维](../../wiki/operations.md) — 配置路径规则、验证/构建脚本、发布检查
|
||
- [API 接入总览](../../wiki/api/00-overview.md) — gRPC / 动态 HTTP / gateway / 鉴权总览
|
||
- 模块级 API(gRPC):`../../wiki/api/01-ads.md`、`03-cms.md`、`04-feedback.md`、`05-initial.md`、`06-passport.md`、`07-sender.md`、`08-address.md`、`09-mall.md`、`10-market.md`、`11-order.md`、`12-wallet.md`
|
||
- 模块级 API(原生 REST):`../../wiki/api/16-fts-rest.md`、`17-logs-rest.md`、`18-mgt-rest.md`
|