Files
full/pkgs/ecmall/AGENT.md

777 lines
65 KiB
Markdown
Raw Permalink 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.
# 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 服务可用 |
| 原生 RESTGin | `/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`
- gRPCunary 拦截器,非匿名方法必须带 `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 三份配置完全相同:单端口 16290dev 库 `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 条 admincreate/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 未注入即 panicfts `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 # Windowsgofmt + 每模块 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 / 鉴权总览
- 模块级 APIgRPC`../../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`