From 13e4b8e3630ca9eb2789fe6b88607724458297f0 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 14 Sep 2026 22:17:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E5=92=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E4=BF=AE=E6=94=B9=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 56 +- go.work | 3 +- {all => pkgs/all}/cmd/main/main.go | 8 +- {all => pkgs/all}/etc/default_dev.yaml | 0 {all => pkgs/all}/go.mod | 34 +- {all => pkgs/all}/go.sum | 0 {all => pkgs/all}/internal/config/config.go | 0 .../all}/internal/config/config_test.go | 0 {all => pkgs/all}/internal/impl/impl.go | 2 +- .../all}/internal/server/authorization.go | 0 .../internal/server/authorization_test.go | 0 {all => pkgs/all}/internal/server/dynamic.go | 0 .../all}/internal/server/dynamic_test.go | 0 {all => pkgs/all}/internal/server/response.go | 0 {all => pkgs/all}/internal/server/server.go | 0 .../all}/internal/server/server_test.go | 0 {all => pkgs/all}/internal/service/address.go | 4 +- {all => pkgs/all}/internal/service/ads.go | 4 +- {all => pkgs/all}/internal/service/cloud.go | 4 +- {all => pkgs/all}/internal/service/cms.go | 4 +- .../all}/internal/service/feedback.go | 4 +- {all => pkgs/all}/internal/service/fts.go | 6 +- {all => pkgs/all}/internal/service/initial.go | 4 +- {all => pkgs/all}/internal/service/logs.go | 4 +- {all => pkgs/all}/internal/service/mall.go | 4 +- {all => pkgs/all}/internal/service/market.go | 4 +- {all => pkgs/all}/internal/service/mgt.go | 6 +- {all => pkgs/all}/internal/service/order.go | 4 +- .../all}/internal/service/passport.go | 6 +- {all => pkgs/all}/internal/service/sender.go | 6 +- {all => pkgs/all}/internal/service/service.go | 4 +- {all => pkgs/all}/internal/service/wallet.go | 6 +- pkgs/ecmall/AGENT.md | 776 ++++++++++++++++++ pkgs/ecmall/cmd/main/main.go | 58 ++ pkgs/ecmall/etc/default_dev.yaml | 150 ++++ pkgs/ecmall/go.mod | 145 ++++ pkgs/ecmall/go.sum | 489 +++++++++++ pkgs/ecmall/internal/config/config.go | 130 +++ pkgs/ecmall/internal/config/config_test.go | 53 ++ pkgs/ecmall/internal/impl/impl.go | 42 + pkgs/ecmall/internal/server/authorization.go | 126 +++ .../internal/server/authorization_test.go | 102 +++ pkgs/ecmall/internal/server/dynamic.go | 194 +++++ pkgs/ecmall/internal/server/dynamic_test.go | 76 ++ pkgs/ecmall/internal/server/response.go | 78 ++ pkgs/ecmall/internal/server/server.go | 133 +++ pkgs/ecmall/internal/server/server_test.go | 24 + pkgs/ecmall/internal/service/address.go | 20 + pkgs/ecmall/internal/service/ads.go | 20 + pkgs/ecmall/internal/service/cms.go | 20 + pkgs/ecmall/internal/service/feedback.go | 20 + pkgs/ecmall/internal/service/fts.go | 16 + pkgs/ecmall/internal/service/initial.go | 20 + pkgs/ecmall/internal/service/logs.go | 14 + pkgs/ecmall/internal/service/mall.go | 20 + pkgs/ecmall/internal/service/market.go | 20 + pkgs/ecmall/internal/service/mgt.go | 16 + pkgs/ecmall/internal/service/order.go | 20 + pkgs/ecmall/internal/service/passport.go | 22 + pkgs/ecmall/internal/service/sender.go | 22 + pkgs/ecmall/internal/service/service.go | 45 + pkgs/ecmall/internal/service/service_test.go | 62 ++ pkgs/ecmall/internal/service/wallet.go | 22 + scripts/verify-workspace.ps1 | 2 +- scripts/verify-workspace.sh | 2 +- wiki/README.md | 1 + wiki/api/00-overview.md | 6 +- wiki/architecture.md | 22 +- wiki/audit-2026-08-10.md | 6 +- wiki/operations.md | 11 +- 70 files changed, 3092 insertions(+), 90 deletions(-) rename {all => pkgs/all}/cmd/main/main.go (86%) rename {all => pkgs/all}/etc/default_dev.yaml (100%) rename {all => pkgs/all}/go.mod (86%) rename {all => pkgs/all}/go.sum (100%) rename {all => pkgs/all}/internal/config/config.go (100%) rename {all => pkgs/all}/internal/config/config_test.go (100%) rename {all => pkgs/all}/internal/impl/impl.go (97%) rename {all => pkgs/all}/internal/server/authorization.go (100%) rename {all => pkgs/all}/internal/server/authorization_test.go (100%) rename {all => pkgs/all}/internal/server/dynamic.go (100%) rename {all => pkgs/all}/internal/server/dynamic_test.go (100%) rename {all => pkgs/all}/internal/server/response.go (100%) rename {all => pkgs/all}/internal/server/server.go (100%) rename {all => pkgs/all}/internal/server/server_test.go (100%) rename {all => pkgs/all}/internal/service/address.go (84%) rename {all => pkgs/all}/internal/service/ads.go (84%) rename {all => pkgs/all}/internal/service/cloud.go (84%) rename {all => pkgs/all}/internal/service/cms.go (84%) rename {all => pkgs/all}/internal/service/feedback.go (84%) rename {all => pkgs/all}/internal/service/fts.go (77%) rename {all => pkgs/all}/internal/service/initial.go (84%) rename {all => pkgs/all}/internal/service/logs.go (83%) rename {all => pkgs/all}/internal/service/mall.go (84%) rename {all => pkgs/all}/internal/service/market.go (84%) rename {all => pkgs/all}/internal/service/mgt.go (77%) rename {all => pkgs/all}/internal/service/order.go (84%) rename {all => pkgs/all}/internal/service/passport.go (80%) rename {all => pkgs/all}/internal/service/sender.go (79%) rename {all => pkgs/all}/internal/service/service.go (92%) rename {all => pkgs/all}/internal/service/wallet.go (79%) create mode 100644 pkgs/ecmall/AGENT.md create mode 100644 pkgs/ecmall/cmd/main/main.go create mode 100644 pkgs/ecmall/etc/default_dev.yaml create mode 100644 pkgs/ecmall/go.mod create mode 100644 pkgs/ecmall/go.sum create mode 100644 pkgs/ecmall/internal/config/config.go create mode 100644 pkgs/ecmall/internal/config/config_test.go create mode 100644 pkgs/ecmall/internal/impl/impl.go create mode 100644 pkgs/ecmall/internal/server/authorization.go create mode 100644 pkgs/ecmall/internal/server/authorization_test.go create mode 100644 pkgs/ecmall/internal/server/dynamic.go create mode 100644 pkgs/ecmall/internal/server/dynamic_test.go create mode 100644 pkgs/ecmall/internal/server/response.go create mode 100644 pkgs/ecmall/internal/server/server.go create mode 100644 pkgs/ecmall/internal/server/server_test.go create mode 100644 pkgs/ecmall/internal/service/address.go create mode 100644 pkgs/ecmall/internal/service/ads.go create mode 100644 pkgs/ecmall/internal/service/cms.go create mode 100644 pkgs/ecmall/internal/service/feedback.go create mode 100644 pkgs/ecmall/internal/service/fts.go create mode 100644 pkgs/ecmall/internal/service/initial.go create mode 100644 pkgs/ecmall/internal/service/logs.go create mode 100644 pkgs/ecmall/internal/service/mall.go create mode 100644 pkgs/ecmall/internal/service/market.go create mode 100644 pkgs/ecmall/internal/service/mgt.go create mode 100644 pkgs/ecmall/internal/service/order.go create mode 100644 pkgs/ecmall/internal/service/passport.go create mode 100644 pkgs/ecmall/internal/service/sender.go create mode 100644 pkgs/ecmall/internal/service/service.go create mode 100644 pkgs/ecmall/internal/service/service_test.go create mode 100644 pkgs/ecmall/internal/service/wallet.go diff --git a/README.md b/README.md index 43c0223..e4b8ad1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # BSM Infra Full -BSM 后端服务 Go workspace,按基础服务、电商交易、资金支付、社交关系四个服务域组织。当前 `module/` 下有 **18 个带 `go.mod` 和独立入口的服务模块**,加上 `all` 共 19 个运行时 Go module;`scripts/api-docgen` 是另一个独立的文档生成工具 module,不属于业务服务。Go 版本以 `go.work` 为准,当前为 1.26.5。 +BSM 后端服务 Go workspace,按基础服务、电商交易、资金支付、社交关系四个服务域组织。当前 `module/` 下有 **18 个带 `go.mod` 和独立入口的服务模块**,加上聚合入口 `pkgs/all` 与电商聚合入口 `pkgs/ecmall` 共 20 个运行时 Go module;`scripts/api-docgen` 是另一个独立的文档生成工具 module,不属于业务服务。Go 版本以 `go.work` 为准,当前为 1.26.5。 -本文依据各模块的 `proto`、路由注册、`internal/logic`、数据模型和 `all/internal/service` 整理。功能说明描述代码职责;对已发现的空实现单独标注,不代表所有接口已经完成端到端验证。 +本文依据各模块的 `proto`、路由注册、`internal/logic`、数据模型和 `pkgs/all/internal/service`、`pkgs/ecmall/internal/service` 整理。功能说明描述代码职责;对已发现的空实现单独标注,不代表所有接口已经完成端到端验证。 ## 快速开始 @@ -21,22 +21,30 @@ Linux/macOS: cd module/base/passport go run ./cmd/main -运行聚合服务: +运行聚合服务(全量): - cd all + cd pkgs/all go run ./cmd/main --workspace=default -all 默认读取 all/etc/default_.yaml。指定 --workspace=tenant01 时,开发模式读取 all/etc/tenant01_dev.yaml。workspace 仅允许字母、数字、下划线和连字符,最长 64 个字符。 +运行电商聚合服务(电商、支付、平台与支撑子模块): + + cd pkgs/ecmall + go run ./cmd/main --workspace=default + +`pkgs/all` 默认读取 `pkgs/all/etc/default_.yaml`,监听 gRPC `12000` / HTTP `12001`。指定 --workspace=tenant01 时,开发模式读取 `pkgs/all/etc/tenant01_dev.yaml`。workspace 仅允许字母、数字、下划线和连字符,最长 64 个字符。 + +`pkgs/ecmall` 使用相同的取值规则读取 `pkgs/ecmall/etc/default_.yaml`,监听 gRPC `12300` / HTTP `12301`,因此两个聚合入口可以同时启动。 ## 服务域与模块总览 -| 服务域 | 目录 | 服务模块 | 主要职责 | 接入 all | +| 服务域 | 目录 | 服务模块 | 主要职责 | 接入聚合入口 | | --- | --- | --- | --- | --- | -| 基础与平台服务 | `module/base` | ads、cloud、cms、feedback、fts、initial、logs、mgt、passport、sender(10 个) | 账号、后台权限、启动配置、内容、文件、通知与运营支撑 | 是 | -| 电商与交易服务 | `module/ec` | address、mall、market、order(4 个) | 收货地址、店铺商品、代理供应商、购物车与订单 | 是 | -| 资金与支付服务 | `module/finance` | wallet(1 个) | 钱包、资金流水、支付渠道、提现申请 | 是 | +| 基础与平台服务 | `module/base` | ads、cloud、cms、feedback、fts、initial、logs、mgt、passport、sender(10 个) | 账号、后台权限、启动配置、内容、文件、通知与运营支撑 | `all`;除 cloud 外均接入 `ecmall` | +| 电商与交易服务 | `module/ec` | address、mall、market、order(4 个) | 收货地址、店铺商品、代理供应商、购物车与订单 | `all`、`ecmall` | +| 资金与支付服务 | `module/finance` | wallet(1 个) | 钱包、资金流水、支付渠道、提现申请 | `all`、`ecmall` | | 社交服务 | `module/social` | feed、group、relation(3 个) | 动态内容、群组成员、好友关注与匹配 | 否 | -| 服务聚合与运行入口 | `all` | all | 组合 15 个基础、电商、资金服务,共享基础设施与访问入口 | 聚合宿主 | +| 服务聚合与运行入口 | `pkgs/all` | all | 组合 15 个基础、电商、资金服务,共享基础设施与访问入口 | 聚合宿主 | +| 电商聚合运行入口 | `pkgs/ecmall` | ecmall | 组合 14 个电商、支付、平台与支撑服务,共享基础设施与独立端口 | 聚合宿主 | 此外,`module/ec/supply` 仅有说明文件,`module/finance/bill`、`module/finance/loan` 在当前工作区为空目录,均不计入已实现服务。具体状态见下文各服务域说明。 @@ -209,9 +217,9 @@ all 默认读取 all/etc/default_.yaml。指定 --workspace=tenant01 - **实现边界**:四类 `Timeline` 方法、`Tag.PostList`、`Post.CommentList`、`Setting.Info` 和 `Setting.Rights` 仍为占位。因此当前不能认定已实现推荐排序、好友/关注动态分发或完整的社区内容流。 - **代码入口**:[接口定义](module/social/feed/proto)、[业务逻辑](module/social/feed/internal/logic)。 -### 聚合运行域:all +### 聚合运行域:pkgs/all -`all` 是部署和接入宿主:在单个进程中注册 15 个基础、电商、资金服务,注入共享数据库、Redis、etcd 和内存缓存,统一管理监听端口与关闭流程。 +`pkgs/all` 是部署和接入宿主(module 路径 `bsm/full/pkgs/all`):在单个进程中注册 15 个基础、电商、资金服务,注入共享数据库、Redis、etcd 和内存缓存,统一管理监听端口与关闭流程。 | 访问方式 | 作用 | | --- | --- | @@ -222,7 +230,22 @@ all 默认读取 all/etc/default_.yaml。指定 --workspace=tenant01 `Services` 配置决定启用哪些服务;非空的 `BSM_SERVICES` 环境变量优先覆盖,例如 `passport,order,wallet`;值为 `all` 时选择注册表中的全部服务。此处的“全部”不包括三个社交服务与预留目录。 -模块通过 `service.Expose` 注册并接收依赖;聚合启动时复用共享对象,独立入口则初始化自身资源。注册入口见 [all/internal/service/service.go](all/internal/service/service.go),监听与请求分发见 [all/internal/server](all/internal/server)。启用模块只代表注册其访问入口,不会自动补齐其外部渠道、数据表或跨域数据依赖。 +模块通过 `service.Expose` 注册并接收依赖;聚合启动时复用共享对象,独立入口则初始化自身资源。注册入口见 [pkgs/all/internal/service/service.go](pkgs/all/internal/service/service.go),监听与请求分发见 [pkgs/all/internal/server](pkgs/all/internal/server)。启用模块只代表注册其访问入口,不会自动补齐其外部渠道、数据表或跨域数据依赖。 + +### 电商聚合运行域:pkgs/ecmall + +`pkgs/ecmall` 是面向电商业务的聚合宿主(module 路径 `bsm/full/pkgs/ecmall`),监听 gRPC `12300` / HTTP `12301`。它复用与 `pkgs/all` 完全相同的监听、鉴权、grpc-gateway、动态 RPC 和原生 REST 分流实现,注册表包含 14 个服务: + +| 分组 | 服务 | 职责 | +| --- | --- | --- | +| 电商与交易 | address、mall、market、order | 收货地址、店铺商品、代理供应商、购物车与订单 | +| 资金与支付 | wallet | 钱包、资金流水、支付渠道、提现申请 | +| 平台与运营 | ads、cms、feedback、fts、logs、mgt | 广告位、内容管理、意见反馈、文件上传、操作日志与后台管理 | +| 基础支撑 | initial、passport、sender | 启动配置与字典、用户身份、验证码与通知发送 | + +`module/base/cloud` 不接入 `ecmall`,`internal/config` 中因而没有对应的配置段;其余专项配置段为 `Fts`、`Mgt`、`Passport`、`Sender`、`Wallet`,并配有对应的匿名白名单。注册入口见 [pkgs/ecmall/internal/service/service.go](pkgs/ecmall/internal/service/service.go)。 + +`ecmall` 只是注册表的取舍,不改变各子模块自身的实现边界:它与 `pkgs/all` 使用同一份 `module/` 代码、同一个共享数据库与 Redis 配置模板,两个入口可以同时启动,也可以分别按 `Services` 或 `BSM_SERVICES` 裁剪。 ## 模块协作与代码导航 @@ -251,7 +274,8 @@ all 默认读取 all/etc/default_.yaml。指定 --workspace=tenant01 | `internal/impl` | 数据库、Redis、etcd、缓存与渠道客户端等资源 | | `internal/config` / `etc` | 配置结构和环境配置样例 | | `service` | 已接入聚合宿主的模块对外暴露的注册与依赖注入接口 | -| `all` | 聚合服务配置、资源初始化和统一入口 | +| `pkgs/all` | 聚合服务配置、资源初始化和统一入口 | +| `pkgs/ecmall` | 电商聚合配置:只注册电商、支付、平台和支撑子模块,端口与 `pkgs/all` 独立 | | `scripts` | workspace 校验、构建、protobuf 和 API 文档生成工具 | | `wiki` | 架构、接入、运维与 API 说明 | @@ -279,13 +303,15 @@ all 默认读取 all/etc/default_.yaml。指定 --workspace=tenant01 - [Wiki 索引](wiki/README.md) - [API 接入与模块文档](wiki/api/00-overview.md) - [系统架构](wiki/architecture.md) +- [电商聚合开发上下文](pkgs/ecmall/AGENT.md) — pkgs/ecmall 的 AGENT 上下文 - [开发与运维](wiki/operations.md) - [审计报告](wiki/audit-2026-08-10.md) ## 开发约定 - 新模块必须加入 go.work,不得提交本机绝对路径 replace。 -- 模块保留独立 cmd/main,通过 service.Expose 接入 all。 +- 模块保留独立 cmd/main,通过 service.Expose 接入聚合入口(`pkgs/all`、`pkgs/ecmall` 或二者)。 +- 聚合入口只做注册表取舍,不得在 `pkgs/*` 内复制子模块业务逻辑。 - 默认测试不得访问网络或真实基础设施;集成测试使用 integration build tag。 - protobuf 全限定名称必须跨模块唯一。 - 提交前运行 workspace 验证脚本和 git diff --check。 diff --git a/go.work b/go.work index 5caf6da..ca184b1 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,6 @@ go 1.26.5 use ( - ./all ./module/base/ads ./module/base/cloud ./module/base/cms @@ -20,6 +19,8 @@ use ( ./module/social/feed ./module/social/group ./module/social/relation + ./pkgs/all + ./pkgs/ecmall ) replace git.apinb.com/bsm-sdk/core => ../../bsm-sdk/core diff --git a/all/cmd/main/main.go b/pkgs/all/cmd/main/main.go similarity index 86% rename from all/cmd/main/main.go rename to pkgs/all/cmd/main/main.go index c9e7606..938ce18 100644 --- a/all/cmd/main/main.go +++ b/pkgs/all/cmd/main/main.go @@ -9,10 +9,10 @@ import ( "syscall" "time" - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - allserver "bsm/full/all/internal/server" - "bsm/full/all/internal/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + allserver "bsm/full/pkgs/all/internal/server" + "bsm/full/pkgs/all/internal/service" "git.apinb.com/bsm-sdk/core/utils" ) diff --git a/all/etc/default_dev.yaml b/pkgs/all/etc/default_dev.yaml similarity index 100% rename from all/etc/default_dev.yaml rename to pkgs/all/etc/default_dev.yaml diff --git a/all/go.mod b/pkgs/all/go.mod similarity index 86% rename from all/go.mod rename to pkgs/all/go.mod index 4fbfc4c..ad30c71 100644 --- a/all/go.mod +++ b/pkgs/all/go.mod @@ -1,24 +1,24 @@ -module bsm/full/all +module bsm/full/pkgs/all go 1.26.5 replace ( - bsm/full/module/base/ads => ../module/base/ads - bsm/full/module/base/cloud => ../module/base/cloud - bsm/full/module/base/cms => ../module/base/cms - bsm/full/module/base/feedback => ../module/base/feedback - bsm/full/module/base/fts => ../module/base/fts - bsm/full/module/base/initial => ../module/base/initial - bsm/full/module/base/logs => ../module/base/logs - bsm/full/module/base/mgt => ../module/base/mgt - bsm/full/module/base/passport => ../module/base/passport - bsm/full/module/base/sender => ../module/base/sender - bsm/full/module/ec/address => ../module/ec/address - bsm/full/module/ec/mall => ../module/ec/mall - bsm/full/module/ec/market => ../module/ec/market - bsm/full/module/ec/order => ../module/ec/order - bsm/full/module/finance/wallet => ../module/finance/wallet - git.apinb.com/bsm-sdk/core => ../../../bsm-sdk/core + bsm/full/module/base/ads => ../../module/base/ads + bsm/full/module/base/cloud => ../../module/base/cloud + bsm/full/module/base/cms => ../../module/base/cms + bsm/full/module/base/feedback => ../../module/base/feedback + bsm/full/module/base/fts => ../../module/base/fts + bsm/full/module/base/initial => ../../module/base/initial + bsm/full/module/base/logs => ../../module/base/logs + bsm/full/module/base/mgt => ../../module/base/mgt + bsm/full/module/base/passport => ../../module/base/passport + bsm/full/module/base/sender => ../../module/base/sender + bsm/full/module/ec/address => ../../module/ec/address + bsm/full/module/ec/mall => ../../module/ec/mall + bsm/full/module/ec/market => ../../module/ec/market + bsm/full/module/ec/order => ../../module/ec/order + bsm/full/module/finance/wallet => ../../module/finance/wallet + git.apinb.com/bsm-sdk/core => ../../../../bsm-sdk/core ) require ( diff --git a/all/go.sum b/pkgs/all/go.sum similarity index 100% rename from all/go.sum rename to pkgs/all/go.sum diff --git a/all/internal/config/config.go b/pkgs/all/internal/config/config.go similarity index 100% rename from all/internal/config/config.go rename to pkgs/all/internal/config/config.go diff --git a/all/internal/config/config_test.go b/pkgs/all/internal/config/config_test.go similarity index 100% rename from all/internal/config/config_test.go rename to pkgs/all/internal/config/config_test.go diff --git a/all/internal/impl/impl.go b/pkgs/all/internal/impl/impl.go similarity index 97% rename from all/internal/impl/impl.go rename to pkgs/all/internal/impl/impl.go index 972eeac..e7f4e14 100644 --- a/all/internal/impl/impl.go +++ b/pkgs/all/internal/impl/impl.go @@ -1,7 +1,7 @@ package impl import ( - "bsm/full/all/internal/config" + "bsm/full/pkgs/all/internal/config" "git.apinb.com/bsm-sdk/core/cache/redis" "git.apinb.com/bsm-sdk/core/with" diff --git a/all/internal/server/authorization.go b/pkgs/all/internal/server/authorization.go similarity index 100% rename from all/internal/server/authorization.go rename to pkgs/all/internal/server/authorization.go diff --git a/all/internal/server/authorization_test.go b/pkgs/all/internal/server/authorization_test.go similarity index 100% rename from all/internal/server/authorization_test.go rename to pkgs/all/internal/server/authorization_test.go diff --git a/all/internal/server/dynamic.go b/pkgs/all/internal/server/dynamic.go similarity index 100% rename from all/internal/server/dynamic.go rename to pkgs/all/internal/server/dynamic.go diff --git a/all/internal/server/dynamic_test.go b/pkgs/all/internal/server/dynamic_test.go similarity index 100% rename from all/internal/server/dynamic_test.go rename to pkgs/all/internal/server/dynamic_test.go diff --git a/all/internal/server/response.go b/pkgs/all/internal/server/response.go similarity index 100% rename from all/internal/server/response.go rename to pkgs/all/internal/server/response.go diff --git a/all/internal/server/server.go b/pkgs/all/internal/server/server.go similarity index 100% rename from all/internal/server/server.go rename to pkgs/all/internal/server/server.go diff --git a/all/internal/server/server_test.go b/pkgs/all/internal/server/server_test.go similarity index 100% rename from all/internal/server/server_test.go rename to pkgs/all/internal/server/server_test.go diff --git a/all/internal/service/address.go b/pkgs/all/internal/service/address.go similarity index 84% rename from all/internal/service/address.go rename to pkgs/all/internal/service/address.go index 41dae40..723d8ce 100644 --- a/all/internal/service/address.go +++ b/pkgs/all/internal/service/address.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/ec/address/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeAddress(srv *server.Server) error { diff --git a/all/internal/service/ads.go b/pkgs/all/internal/service/ads.go similarity index 84% rename from all/internal/service/ads.go rename to pkgs/all/internal/service/ads.go index 1b0772b..f4dfd13 100644 --- a/all/internal/service/ads.go +++ b/pkgs/all/internal/service/ads.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/ads/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeAds(srv *server.Server) error { diff --git a/all/internal/service/cloud.go b/pkgs/all/internal/service/cloud.go similarity index 84% rename from all/internal/service/cloud.go rename to pkgs/all/internal/service/cloud.go index b569202..8eaa593 100644 --- a/all/internal/service/cloud.go +++ b/pkgs/all/internal/service/cloud.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/cloud/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeCloud(srv *server.Server) error { diff --git a/all/internal/service/cms.go b/pkgs/all/internal/service/cms.go similarity index 84% rename from all/internal/service/cms.go rename to pkgs/all/internal/service/cms.go index 39d6814..44b4330 100644 --- a/all/internal/service/cms.go +++ b/pkgs/all/internal/service/cms.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/cms/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeCMS(srv *server.Server) error { diff --git a/all/internal/service/feedback.go b/pkgs/all/internal/service/feedback.go similarity index 84% rename from all/internal/service/feedback.go rename to pkgs/all/internal/service/feedback.go index b17a560..9e70d76 100644 --- a/all/internal/service/feedback.go +++ b/pkgs/all/internal/service/feedback.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/feedback/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeFeedback(srv *server.Server) error { diff --git a/all/internal/service/fts.go b/pkgs/all/internal/service/fts.go similarity index 77% rename from all/internal/service/fts.go rename to pkgs/all/internal/service/fts.go index bf753cb..bf27c6f 100644 --- a/all/internal/service/fts.go +++ b/pkgs/all/internal/service/fts.go @@ -1,10 +1,10 @@ package service import ( - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/fts/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeFTS(srv *server.Server) error { diff --git a/all/internal/service/initial.go b/pkgs/all/internal/service/initial.go similarity index 84% rename from all/internal/service/initial.go rename to pkgs/all/internal/service/initial.go index cc2ea0d..4ef771e 100644 --- a/all/internal/service/initial.go +++ b/pkgs/all/internal/service/initial.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/initial/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeInitial(srv *server.Server) error { diff --git a/all/internal/service/logs.go b/pkgs/all/internal/service/logs.go similarity index 83% rename from all/internal/service/logs.go rename to pkgs/all/internal/service/logs.go index b143681..75a5d6a 100644 --- a/all/internal/service/logs.go +++ b/pkgs/all/internal/service/logs.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/logs/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeLogs(srv *server.Server) error { diff --git a/all/internal/service/mall.go b/pkgs/all/internal/service/mall.go similarity index 84% rename from all/internal/service/mall.go rename to pkgs/all/internal/service/mall.go index 52be5ec..8343590 100644 --- a/all/internal/service/mall.go +++ b/pkgs/all/internal/service/mall.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/ec/mall/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeMall(srv *server.Server) error { diff --git a/all/internal/service/market.go b/pkgs/all/internal/service/market.go similarity index 84% rename from all/internal/service/market.go rename to pkgs/all/internal/service/market.go index 1435a73..9cb052b 100644 --- a/all/internal/service/market.go +++ b/pkgs/all/internal/service/market.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/ec/market/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeMarket(srv *server.Server) error { diff --git a/all/internal/service/mgt.go b/pkgs/all/internal/service/mgt.go similarity index 77% rename from all/internal/service/mgt.go rename to pkgs/all/internal/service/mgt.go index 92564cd..7a5ac43 100644 --- a/all/internal/service/mgt.go +++ b/pkgs/all/internal/service/mgt.go @@ -1,10 +1,10 @@ package service import ( - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/mgt/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeMgt(srv *server.Server) error { diff --git a/all/internal/service/order.go b/pkgs/all/internal/service/order.go similarity index 84% rename from all/internal/service/order.go rename to pkgs/all/internal/service/order.go index 41adcf5..d452097 100644 --- a/all/internal/service/order.go +++ b/pkgs/all/internal/service/order.go @@ -1,9 +1,9 @@ package service import ( - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/ec/order/service" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeOrder(srv *server.Server) error { diff --git a/all/internal/service/passport.go b/pkgs/all/internal/service/passport.go similarity index 80% rename from all/internal/service/passport.go rename to pkgs/all/internal/service/passport.go index 8690812..fd90aad 100644 --- a/all/internal/service/passport.go +++ b/pkgs/all/internal/service/passport.go @@ -1,10 +1,10 @@ package service import ( - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/passport/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposePassport(srv *server.Server) error { diff --git a/all/internal/service/sender.go b/pkgs/all/internal/service/sender.go similarity index 79% rename from all/internal/service/sender.go rename to pkgs/all/internal/service/sender.go index b592867..e2c8d5f 100644 --- a/all/internal/service/sender.go +++ b/pkgs/all/internal/service/sender.go @@ -1,10 +1,10 @@ package service import ( - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/base/sender/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeSender(srv *server.Server) error { diff --git a/all/internal/service/service.go b/pkgs/all/internal/service/service.go similarity index 92% rename from all/internal/service/service.go rename to pkgs/all/internal/service/service.go index 14efb3c..a8358e6 100644 --- a/all/internal/service/service.go +++ b/pkgs/all/internal/service/service.go @@ -3,8 +3,8 @@ package service import ( "fmt" - "bsm/full/all/internal/config" - "bsm/full/all/internal/server" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/server" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" ) diff --git a/all/internal/service/wallet.go b/pkgs/all/internal/service/wallet.go similarity index 79% rename from all/internal/service/wallet.go rename to pkgs/all/internal/service/wallet.go index e18d8c4..c365241 100644 --- a/all/internal/service/wallet.go +++ b/pkgs/all/internal/service/wallet.go @@ -1,10 +1,10 @@ package service import ( - "bsm/full/all/internal/config" - "bsm/full/all/internal/impl" - "bsm/full/all/internal/server" moduleService "bsm/full/module/finance/wallet/service" + "bsm/full/pkgs/all/internal/config" + "bsm/full/pkgs/all/internal/impl" + "bsm/full/pkgs/all/internal/server" ) func exposeWallet(srv *server.Server) error { diff --git a/pkgs/ecmall/AGENT.md b/pkgs/ecmall/AGENT.md new file mode 100644 index 0000000..c0b2f1c --- /dev/null +++ b/pkgs/ecmall/AGENT.md @@ -0,0 +1,776 @@ +# 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`): + +``` +/etc/<小写 serviceKey>_.yaml +``` + +- 文件不存在时回退到 `/etc/workspace__.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 = `(`: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./`) +- **表**: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./` 脱节,实测会 404) + +### 9.3 feedback — 用户反馈工单 + +- **暴露**:`Method`(6):List/Get/Add/Modify/Delete/Remark;无 `Config`;gRPC + gateway `POST /feedback.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/_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` diff --git a/pkgs/ecmall/cmd/main/main.go b/pkgs/ecmall/cmd/main/main.go new file mode 100644 index 0000000..2353755 --- /dev/null +++ b/pkgs/ecmall/cmd/main/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "flag" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + ecmallserver "bsm/full/pkgs/ecmall/internal/server" + "bsm/full/pkgs/ecmall/internal/service" + + "git.apinb.com/bsm-sdk/core/utils" +) + +var ServiceKey = "default" + +func main() { + workspace := flag.String("workspace", ServiceKey, "workspace used to select etc/{workspace}_{runtime}.yaml") + flag.Parse() + value, err := utils.MustString(*workspace) + if err != nil { + panic(err) + } + ServiceKey = value + + config.New(ServiceKey) + impl.NewImpl() + + srv, err := ecmallserver.New( + config.Spec.Authorization.Key, + config.Spec.Authorization.Expire, + config.Spec.Authorization.Anonymous, + ) + if err != nil { + panic(err) + } + if err := service.Expose(srv); err != nil { + panic(err) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-stop + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = srv.Stop(ctx) + }() + + if err := srv.Start(config.Spec.Server.GRPC.Addr, config.Spec.Server.HTTP.Addr); err != nil && err != http.ErrServerClosed { + panic(err) + } +} diff --git a/pkgs/ecmall/etc/default_dev.yaml b/pkgs/ecmall/etc/default_dev.yaml new file mode 100644 index 0000000..3e62ae2 --- /dev/null +++ b/pkgs/ecmall/etc/default_dev.yaml @@ -0,0 +1,150 @@ +Service: default +Server: + GRPC: + BindIP: 0.0.0.0 + Port: 12300 + HTTP: + BindIP: 0.0.0.0 + Port: 12301 + + +Databases: + Driver: postgres + Source: + - host=127.0.0.1 user=postgres password=Weidong1214 dbname=bsm_dev port=5432 sslmode=disable TimeZone=Asia/Shanghai + +Cache: redis://null:Weidong1214@127.0.0.1:6379/1 +SecretKey: CHANGE_ME + +# Authorization contains the HS256 signing key and maximum token lifetime in +# seconds. Clients send the raw JWT in the Authorization header (no Bearer). +Authorization: + Key: CHANGE_ME_32_BYTE_JWT_SECRET_KEY + Expire: 86400 + Anonymous: + - /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 + +# Enabled services. ecmall registers the e-commerce domain (address, mall, +# market, order), the payment domain (wallet), the platform domain (ads, cms, +# feedback, fts, logs, mgt) and the base support services they rely on +# (initial, passport, sender). Use "all" to enable every registered service. +# A non-empty BSM_SERVICES overrides this list. +Services: + - ads + - cms + - feedback + - fts + - initial + - logs + - mgt + - passport + - sender + - address + - mall + - market + - order + - wallet + +# Service-specific configuration is flat at the document root. Shared +# database, Redis, RPC, APM and Etcd settings above are assigned automatically. +# Fts / Mgt are passed to their modules through ExposeOptions.Config. Note that +# the aggregate never runs a module's model migration or seeding, so +# Mgt.InitRootUser does not create the root administrator here (see AGENT.md §3.5). +Fts: + MinioOss: + Site: http://127.0.0.1:9000 + Endpoint: 127.0.0.1:9000 + AccessKeyId: CHANGE_ME + AccessKeySecret: CHANGE_ME + UseSSL: false + Local: + Site: http://127.0.0.1:12301/files + UploadDir: ./uploader/ + FtsConfig: + InputKey: file + MaxSize: 5368709120 + Allows: [.doc, .docx, .txt, .png, .jpg, .jpeg, .gif, .pdf, .xls, .xlsx, .ppt, .pptx, .mp4, .avi, .mkv, .webp, .mov, .wmv, .mp3, .wav] + +Mgt: + InitRootUser: true + SecretKey: CHANGE_ME + +Passport: + Token: + Prefix: /token/ + Expire: 86400 + Kyc: + Provider: jumio + BaseUrl: CHANGE_ME + ApiSecret: CHANGE_ME + ApiToken: CHANGE_ME + ApiArgs: CHANGE_ME + WeChatConf: + AppID: CHANGE_ME + AppSecret: CHANGE_ME + +Sender: + SMTP: + default: + Endpoint: smtp.example.com + Port: 465 + Username: CHANGE_ME + Password: CHANGE_ME + FromAddress: no-reply@example.com + FromName: BSM + SMS: + default: + Endpoint: CHANGE_ME + AccessKeyId: CHANGE_ME + AccessKeySecret: CHANGE_ME + Region: CHANGE_ME + Code: + Length: 6 + Expire: 300 + MaxSentLimit: 5 + CokeyKey: code + BlackListFilter: [127.0.0.1] + +Wallet: + QrCodeSavePath: ./qrcode/ + WeChat: + AppID: CHANGE_ME + AppSecret: CHANGE_ME + SerialNo: CHANGE_ME + APIV3Key: CHANGE_ME + PrivateKey: CHANGE_ME + NotifyURL: CHANGE_ME + Currency: CNY + MchID: CHANGE_ME + Alipay: + AppID: CHANGE_ME + AppSecret: CHANGE_ME + PrivateKey: CHANGE_ME + IsProd: false + NotifyURL: CHANGE_ME + ReturnURL: CHANGE_ME + QuitURL: CHANGE_ME + Wallet: + Name: default + URL: CHANGE_ME + AlipyIsOpen: false + WalletIsOpen: false + WechatpayIsOpen: false diff --git a/pkgs/ecmall/go.mod b/pkgs/ecmall/go.mod new file mode 100644 index 0000000..956edb1 --- /dev/null +++ b/pkgs/ecmall/go.mod @@ -0,0 +1,145 @@ +module bsm/full/pkgs/ecmall + +go 1.26.5 + +replace ( + bsm/full/module/base/ads => ../../module/base/ads + bsm/full/module/base/cms => ../../module/base/cms + bsm/full/module/base/feedback => ../../module/base/feedback + bsm/full/module/base/fts => ../../module/base/fts + bsm/full/module/base/initial => ../../module/base/initial + bsm/full/module/base/logs => ../../module/base/logs + bsm/full/module/base/mgt => ../../module/base/mgt + bsm/full/module/base/passport => ../../module/base/passport + bsm/full/module/base/sender => ../../module/base/sender + bsm/full/module/ec/address => ../../module/ec/address + bsm/full/module/ec/mall => ../../module/ec/mall + bsm/full/module/ec/market => ../../module/ec/market + bsm/full/module/ec/order => ../../module/ec/order + bsm/full/module/finance/wallet => ../../module/finance/wallet + git.apinb.com/bsm-sdk/core => ../../../../bsm-sdk/core +) + +require ( + bsm/full/module/base/ads v0.0.0 + bsm/full/module/base/cms v0.0.0 + bsm/full/module/base/feedback v0.0.0 + bsm/full/module/base/fts v0.0.0 + bsm/full/module/base/initial v0.0.0 + bsm/full/module/base/logs v0.0.0 + bsm/full/module/base/mgt v0.0.0 + bsm/full/module/base/passport v0.0.0 + bsm/full/module/base/sender v0.0.0 + bsm/full/module/ec/address v0.0.0 + bsm/full/module/ec/mall v0.0.0 + bsm/full/module/ec/market v0.0.0 + bsm/full/module/ec/order v0.0.0 + bsm/full/module/finance/wallet v0.0.0 + git.apinb.com/bsm-sdk/core v0.2.1 + github.com/gin-contrib/sessions v1.1.1 + github.com/gin-gonic/gin v1.12.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 + github.com/patrickmn/go-cache v2.1.0+incompatible + go.etcd.io/etcd/client/v3 v3.7.1 + go.yaml.in/yaml/v3 v3.0.5 + google.golang.org/grpc v1.83.2 + gorm.io/gorm v1.31.2 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gin-contrib/cors v1.7.8 // indirect + github.com/klauspost/compress v1.20.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.3.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.6.4 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + golang.org/x/net v0.59.0 // indirect +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8 // indirect + github.com/alibabacloud-go/tea v1.5.3 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect + github.com/aliyun/credentials-go v1.4.13 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.4 // indirect + github.com/bytedance/sonic/loader v0.5.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/cloudwego/base64x v0.1.7 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect + github.com/gin-contrib/sse v1.1.2 // indirect + github.com/go-pay/crypto v0.0.3 // indirect + github.com/go-pay/errgroup v0.0.3 // indirect + github.com/go-pay/gopay v1.5.123 // indirect + github.com/go-pay/smap v0.0.2 // indirect + github.com/go-pay/util v0.0.4 // indirect + github.com/go-pay/xlog v0.0.3 // indirect + github.com/go-pay/xtime v0.0.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.4 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.11.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/leodido/go-urn v1.5.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.62.0 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.178 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.2 // indirect + go.etcd.io/etcd/api/v3 v3.7.1 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.7.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.9.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/arch v0.31.0 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd // indirect + google.golang.org/protobuf v1.36.12 + gopkg.in/ini.v1 v1.67.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.2 // indirect +) diff --git a/pkgs/ecmall/go.sum b/pkgs/ecmall/go.sum new file mode 100644 index 0000000..6f81b9d --- /dev/null +++ b/pkgs/ecmall/go.sum @@ -0,0 +1,489 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 h1:o6veen0IZ/Fe1JawwhwQMZcbw67CVDY1pQwXcNWPyQo= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8 h1:aDPyz6C+nenypx24N5qEt09NjpS6mu7Cu1A+wf9UTaY= +github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8/go.mod h1:e/vWJ5gLVnraPROSh+3oMSodf5ukaUlqNgH0IIcnz98= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0= +github.com/alibabacloud-go/tea v1.5.3 h1:UMJTBcO48w7Zz1nlFe9bmRsHwgB7AjF8DBQzcvHhIp8= +github.com/alibabacloud-go/tea v1.5.3/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOqY6Eq8f3zfA= +github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/aliyun/credentials-go v1.4.13 h1:alJaUIolzjrw0sZjOTwYpI34Djqo3MJJh4q+yqMah7Q= +github.com/aliyun/credentials-go v1.4.13/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.4 h1:FgtV/4aBHpla9AxuMpuuzVUpa/Cf3izufkxNmnEzdI8= +github.com/bytedance/sonic v1.15.4/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc= +github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo= +github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= +github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= +github.com/gin-contrib/cors v1.7.8 h1:U0jjyXlWXMEx27hFE6hmdg/TOZviYgsOGGT+kLmUZrc= +github.com/gin-contrib/cors v1.7.8/go.mod h1:u3nLI2pP1IlJn7tbvL7iiubDO9quBZ8FXP9LxmqpsPI= +github.com/gin-contrib/sessions v1.1.1 h1:/3ORwmsZ0h/Kcc9fekFrSSc6QXEifMscBLcS8lyQfqQ= +github.com/gin-contrib/sessions v1.1.1/go.mod h1:h8ilnqzj7fPELlu4/rbFcZW/0SNYMXhm4X/KQv0i1G8= +github.com/gin-contrib/sse v1.1.2 h1:MU2fgl1RrdYTMcgJLtz2kJF+vPg3xrqaaKfUUU18tCo= +github.com/gin-contrib/sse v1.1.2/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-pay/crypto v0.0.3 h1:PDYO7OduTBSSAujF/OLqMRFlcqcebEmAYNRKeTi1rdk= +github.com/go-pay/crypto v0.0.3/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes= +github.com/go-pay/errgroup v0.0.3 h1:DB4s8e8oWYDyETKQ1y1riMJ7y29zE1uIsMCSjEOFSbU= +github.com/go-pay/errgroup v0.0.3/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8= +github.com/go-pay/gopay v1.5.123 h1:ucE5YPc6S98bLSPsgCg5p3niU7dJiylRmnFuR8evrvI= +github.com/go-pay/gopay v1.5.123/go.mod h1:7LD6uIDqNUHCL5OaB/YcCwA37rh+qPgHeS9MWodTDmY= +github.com/go-pay/smap v0.0.2 h1:kKflYor5T5FgZltPFBMTFfjJvqYMHr5VnIFSEyhVTcA= +github.com/go-pay/smap v0.0.2/go.mod h1:HW9oAo0okuyDYsbpbj5fJFxnNj/BZorRGFw26SxrNWw= +github.com/go-pay/util v0.0.4 h1:TuwSU9o3Qd7m9v1PbzFuIA/8uO9FJnA6P7neG/NwPyk= +github.com/go-pay/util v0.0.4/go.mod h1:Tsdhs8Ib9J9b4+NKNO1PHh5hWHhlg98PthsX0ckq6PM= +github.com/go-pay/xlog v0.0.3 h1:avyMhCL/JgBHreoGx/am/kHxfs1udDOAeVqbmzP/Yes= +github.com/go-pay/xlog v0.0.3/go.mod h1:mH47xbobrdsSHWsmFtSF5agWbMHFP+tK0ZbVCk5OAEw= +github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM= +github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w= +github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= +github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU= +github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8= +github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.172/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.178 h1:CX2pX4NjA7JtbB0edCWYpRIhk/oO0JdhbdjkL6pnKAg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.178/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172 h1:8YBaibwP8YXcYP+4qdgoqIwNULYNt5/AyVP7t3J3hCA= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172/go.mod h1:eLllmEVIjbeQpcXHpd/68caL4/EgHBbHruRCmExcv+M= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc= +github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.etcd.io/etcd/api/v3 v3.7.1 h1:KJG0/DcWGfe3Y1otDf/fsBf0TSSgpxZ5RO/L8SFt73E= +go.etcd.io/etcd/api/v3 v3.7.1/go.mod h1:8bXIpCMeV7E3/XL0Ix123ATn3dB+0V7d9zklHbB0m78= +go.etcd.io/etcd/client/pkg/v3 v3.7.1 h1:rKYsj3pRkR0eK3yjT3XOgrhqfmIfj9pzNgxjh7mfFv4= +go.etcd.io/etcd/client/pkg/v3 v3.7.1/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= +go.etcd.io/etcd/client/v3 v3.7.1 h1:0PEMMC0KuZmVIN+RAbdqfkZ45pYTgKVtmBEbRCvZFUg= +go.etcd.io/etcd/client/v3 v3.7.1/go.mod h1:ffNqALa8tRCYhYo1F9oR489y23K39Gz+BSR3ApAGYq0= +go.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ= +go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/arch v0.31.0 h1:22MlEb14/O/EPCYHFxsDdv5TuLD5dMjT5e2QeJw4ULk= +golang.org/x/arch v0.31.0/go.mod h1:KcJSod3cqT2dKcjBxqTyGfbumNikqU9p5tHJinPJnuY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd h1:jgCQeWccunvocKNK2RjhQHNiSjSeMMuBpQ/5pbP+2lA= +google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:r4KD2hOq82JBWpTWkJ9NZLf6EwmRvAPIFmk7hPNtd+0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd h1:29LZPNIJg+GEmJwZXruJJRwqy/wnWAYZXDtROToV/aQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.2 h1:BvXQ/cNUg63q5TFNg672DmDcowZSFrNLkkA3Xe6GXq4= +gorm.io/driver/postgres v1.6.2/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkgs/ecmall/internal/config/config.go b/pkgs/ecmall/internal/config/config.go new file mode 100644 index 0000000..430d60a --- /dev/null +++ b/pkgs/ecmall/internal/config/config.go @@ -0,0 +1,130 @@ +package config + +import ( + "net" + "os" + "strings" + "time" + + ftsService "bsm/full/module/base/fts/service" + mgtService "bsm/full/module/base/mgt/service" + passportService "bsm/full/module/base/passport/service" + senderService "bsm/full/module/base/sender/service" + walletService "bsm/full/module/finance/wallet/service" + "git.apinb.com/bsm-sdk/core/conf" + "git.apinb.com/bsm-sdk/core/env" + "git.apinb.com/bsm-sdk/core/printer" + coreVars "git.apinb.com/bsm-sdk/core/vars" +) + +// SrvConfig is the ecmall aggregate configuration. It only carries the +// services registered by pkgs/ecmall: the e-commerce domain (address, mall, +// market, order), the payment domain (wallet), the platform domain (ads, cms, +// feedback, fts, logs, mgt) and the base support services they rely on +// (initial, passport, sender). Modules that are not part of ecmall keep their +// own configuration in their own process. +type SrvConfig struct { + conf.Base `yaml:",inline"` + Server ServerConfig `yaml:"Server"` + Authorization AuthorizationConfig `yaml:"Authorization"` + Databases *conf.DBConf `yaml:"Databases"` + MicroService *conf.MicroServiceConf `yaml:"MicroService"` + Rpc map[string]conf.RpcConf `yaml:"Rpc"` + Apm *conf.ApmConf `yaml:"APM"` + Etcd *conf.EtcdConf `yaml:"Etcd"` + Services []string `yaml:"Services"` + Fts *ftsService.Config `yaml:"Fts"` + Mgt *mgtService.Config `yaml:"Mgt"` + Passport *passportService.Config `yaml:"Passport"` + Sender *senderService.Config `yaml:"Sender"` + Wallet *walletService.Config `yaml:"Wallet"` +} + +type ListenerConfig struct { + BindIP string `yaml:"BindIP"` + Port string `yaml:"Port"` + Addr string `yaml:"-"` +} + +type ServerConfig struct { + GRPC ListenerConfig `yaml:"GRPC"` + HTTP ListenerConfig `yaml:"HTTP"` +} + +type AuthorizationConfig struct { + Key string `yaml:"Key"` + Expire int64 `yaml:"Expire"` + Anonymous []string `yaml:"Anonymous"` +} + +var Spec SrvConfig + +func New(serviceKey string) { + conf.New(serviceKey, &Spec) + normalizeListener(&Spec.Server.GRPC) + normalizeListener(&Spec.Server.HTTP) + if Spec.Server.GRPC.Addr == Spec.Server.HTTP.Addr { + panic("gRPC and HTTP listeners must use different addresses") + } + if strings.TrimSpace(Spec.Authorization.Key) == "" { + panic("Authorization.Key must not be empty") + } + keyLength := len(Spec.Authorization.Key) + if keyLength != 16 && keyLength != 24 && keyLength != 32 { + panic("Authorization.Key must contain 16, 24, or 32 bytes") + } + if Spec.Authorization.Expire <= 0 { + panic("Authorization.Expire must be greater than zero") + } + env.NewEnv().JwtSecretKey = Spec.Authorization.Key + coreVars.JwtExpire = time.Duration(Spec.Authorization.Expire) * time.Second + // Keep the embedded base address meaningful for module configurations that + // still consume it, while ecmall itself uses the two explicit listeners. + Spec.BindIP, Spec.Port, Spec.Addr = Spec.Server.HTTP.BindIP, Spec.Server.HTTP.Port, Spec.Server.HTTP.Addr + conf.NotNil(Spec.Service, Spec.Cache) + assignSharedConfig() + conf.PrintInfo(Spec.Server.GRPC.Addr) + printer.Info("gRPC Address: %s", Spec.Server.GRPC.Addr) + printer.Info("HTTP Address: %s", Spec.Server.HTTP.Addr) +} + +func normalizeListener(listener *ListenerConfig) { + listener.Port = conf.CheckPort(listener.Port) + listener.BindIP = conf.CheckIP(listener.BindIP) + listener.Addr = net.JoinHostPort(listener.BindIP, listener.Port) +} + +func assignSharedConfig() { + if Spec.Fts != nil { + Spec.Fts.Base, Spec.Fts.Databases, Spec.Fts.Rpc, Spec.Fts.Apm, Spec.Fts.Etcd = Spec.Base, Spec.Databases, Spec.Rpc, Spec.Apm, Spec.Etcd + } + if Spec.Mgt != nil { + Spec.Mgt.Base, Spec.Mgt.Databases, Spec.Mgt.Rpc, Spec.Mgt.Apm, Spec.Mgt.Etcd = Spec.Base, Spec.Databases, Spec.Rpc, Spec.Apm, Spec.Etcd + } + if Spec.Passport != nil { + Spec.Passport.Base, Spec.Passport.Databases, Spec.Passport.MicroService, Spec.Passport.Rpc, Spec.Passport.Apm, Spec.Passport.Etcd = Spec.Base, Spec.Databases, Spec.MicroService, Spec.Rpc, Spec.Apm, Spec.Etcd + } + if Spec.Sender != nil { + Spec.Sender.Base, Spec.Sender.Databases, Spec.Sender.MicroService, Spec.Sender.Rpc, Spec.Sender.Apm, Spec.Sender.Etcd = Spec.Base, Spec.Databases, Spec.MicroService, Spec.Rpc, Spec.Apm, Spec.Etcd + } + if Spec.Wallet != nil { + Spec.Wallet.Base, Spec.Wallet.Databases, Spec.Wallet.MicroService, Spec.Wallet.Rpc, Spec.Wallet.Apm, Spec.Wallet.Etcd = Spec.Base, Spec.Databases, Spec.MicroService, Spec.Rpc, Spec.Apm, Spec.Etcd + } +} + +func Enabled(name string) bool { + if raw := strings.TrimSpace(os.Getenv("BSM_SERVICES")); raw != "" { + for _, selected := range strings.Split(raw, ",") { + if strings.EqualFold(strings.TrimSpace(selected), name) || strings.EqualFold(strings.TrimSpace(selected), "all") { + return true + } + } + return false + } + for _, service := range Spec.Services { + if strings.EqualFold(strings.TrimSpace(service), name) || strings.EqualFold(strings.TrimSpace(service), "all") { + return true + } + } + return false +} diff --git a/pkgs/ecmall/internal/config/config_test.go b/pkgs/ecmall/internal/config/config_test.go new file mode 100644 index 0000000..5f65dc4 --- /dev/null +++ b/pkgs/ecmall/internal/config/config_test.go @@ -0,0 +1,53 @@ +package config + +import ( + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "go.yaml.in/yaml/v3" +) + +// ecmallServices is the aggregate registry of pkgs/ecmall. Keep it in sync with +// the services table in internal/service. +var ecmallServices = []string{"address", "ads", "cms", "feedback", "fts", "initial", "logs", "mgt", "mall", "market", "order", "passport", "sender", "wallet"} + +func TestEcmallDevConfig(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "etc", "default_dev.yaml")) + if err != nil { + t.Fatal(err) + } + var cfg SrvConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + t.Fatal(err) + } + if len(cfg.Services) == 0 { + t.Fatal("services must not be empty") + } + if cfg.Server.GRPC.Port == "" || cfg.Server.HTTP.Port == "" || cfg.Server.GRPC.Port == cfg.Server.HTTP.Port { + t.Fatal("separate gRPC and HTTP ports are required") + } + if cfg.Authorization.Key == "" || cfg.Authorization.Expire <= 0 { + t.Fatal("authorization key and expiration are required") + } + keyLength := len(cfg.Authorization.Key) + if keyLength != 16 && keyLength != 24 && keyLength != 32 { + t.Fatal("authorization key must be compatible with the JWT issuer") + } + if cfg.Fts == nil || cfg.Mgt == nil || cfg.Passport == nil || cfg.Sender == nil || cfg.Wallet == nil { + t.Fatal("service-specific configuration is incomplete") + } + + enabled := make([]string, 0, len(cfg.Services)) + for _, service := range cfg.Services { + enabled = append(enabled, strings.ToLower(strings.TrimSpace(service))) + } + sort.Strings(enabled) + expected := append([]string(nil), ecmallServices...) + sort.Strings(expected) + if strings.Join(enabled, ",") != strings.Join(expected, ",") { + t.Fatalf("ecmall must only enable %v, got %v", expected, enabled) + } +} diff --git a/pkgs/ecmall/internal/impl/impl.go b/pkgs/ecmall/internal/impl/impl.go new file mode 100644 index 0000000..d1d2a32 --- /dev/null +++ b/pkgs/ecmall/internal/impl/impl.go @@ -0,0 +1,42 @@ +package impl + +import ( + "bsm/full/pkgs/ecmall/internal/config" + + "git.apinb.com/bsm-sdk/core/cache/redis" + "git.apinb.com/bsm-sdk/core/with" + "github.com/patrickmn/go-cache" + clientv3 "go.etcd.io/etcd/client/v3" + "gorm.io/gorm" +) + +var ( + // RedisService Redis缓存服务客户端,用于存储临时数据和缓存热点数据 + RedisService *redis.RedisClient + + // EtcdService Etcd客户端,用于服务发现和配置管理 + EtcdService *clientv3.Client + + // DBService 数据库服务连接, 用于数据持久化存储 + DBService *gorm.DB + + // MemoryService 内存缓存服务, 用于存储频繁访问的数据,提供最快的访问速度 + MemoryService *cache.Cache +) + +// NewImpl 初始化实现层 +// 建立与各种外部服务的连接,包括内存缓存、Redis、数据库和Etcd +// 这些连接将在整个服务生命周期中使用 +func NewImpl() { + // 初始化内存缓存服务,用于存储热点数据 + MemoryService = with.Memory(nil) + + // 初始化Redis缓存服务,用于分布式缓存 + RedisService = with.RedisCache(config.Spec.Cache) + + // 初始化数据库连接,用于数据持久化 + DBService = with.Databases(config.Spec.Databases, nil) + + // 初始化Etcd客户端,用于服务发现和配置管理 + EtcdService = with.Etcd(config.Spec.Etcd) +} diff --git a/pkgs/ecmall/internal/server/authorization.go b/pkgs/ecmall/internal/server/authorization.go new file mode 100644 index 0000000..7f4eb7d --- /dev/null +++ b/pkgs/ecmall/internal/server/authorization.go @@ -0,0 +1,126 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "path" + "strings" + "time" + + "git.apinb.com/bsm-sdk/core/errcode" + "github.com/golang-jwt/jwt/v5" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +type authorization struct { + key []byte + expire time.Duration + anonymous map[string]struct{} +} + +func newAuthorization(key string, expireSeconds int64, anonymous []string) (*authorization, error) { + key = strings.TrimSpace(key) + if key == "" { + return nil, errcode.ErrTokenSecretKeyNotFound + } + if expireSeconds <= 0 { + return nil, errcode.ErrTokenAuthExpire + } + allowed := make(map[string]struct{}, len(anonymous)) + for _, item := range anonymous { + if item = normalizePath(item); item != "" { + allowed[item] = struct{}{} + } + } + return &authorization{key: []byte(key), expire: time.Duration(expireSeconds) * time.Second, anonymous: allowed}, nil +} + +func (a *authorization) unaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + if !a.isAnonymous(info.FullMethod) { + values := metadata.ValueFromIncomingContext(ctx, "authorization") + if len(values) == 0 { + return nil, errcode.ErrHeaderAuthorization + } + if err := a.validate(values[0]); err != nil { + return nil, err + } + } + return handler(ctx, req) +} + +func (a *authorization) httpMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestPath := canonicalHTTPPath(r.URL.Path) + if !a.isAnonymous(requestPath) { + if err := a.validate(r.Header.Get("Authorization")); err != nil { + writeHTTPAuthorizationError(w, err) + return + } + } + next.ServeHTTP(w, r) + }) +} + +func (a *authorization) validate(raw string) error { + raw = strings.TrimSpace(raw) + if raw == "" { + return errcode.ErrHeaderAuthorization + } + claims := &jwt.RegisteredClaims{} + tokenValue, err := jwt.ParseWithClaims(raw, claims, func(tokenValue *jwt.Token) (any, error) { + if tokenValue.Method != jwt.SigningMethodHS256 { + return nil, fmt.Errorf("unexpected signing method: %s", tokenValue.Method.Alg()) + } + return a.key, nil + }, jwt.WithExpirationRequired(), jwt.WithIssuedAt(), jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) + if err != nil || !tokenValue.Valid { + if errors.Is(err, jwt.ErrTokenExpired) { + return errcode.ErrTokenAuthExpire + } + return errcode.ErrTokenAuthParseFail + } + if claims.IssuedAt == nil { + return errcode.ErrTokenDataInvalid + } + now := time.Now() + if claims.IssuedAt.Time.After(now) || now.Sub(claims.IssuedAt.Time) > a.expire { + return errcode.ErrTokenAuthExpire + } + return nil +} + +func (a *authorization) isAnonymous(requestPath string) bool { + requestPath = normalizePath(requestPath) + if strings.HasPrefix(requestPath, "/grpc.reflection.") || strings.HasPrefix(requestPath, "/grpc.health.") { + return true + } + _, ok := a.anonymous[requestPath] + return ok +} + +func canonicalHTTPPath(requestPath string) string { + normalized := normalizePath(requestPath) + parts := strings.Split(strings.TrimPrefix(normalized, "/"), "/") + if len(parts) == 4 && parts[0] == "rpc" { + return "/" + parts[1] + "." + parts[2] + "/" + parts[3] + } + return normalized +} + +func normalizePath(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + return path.Clean("/" + strings.TrimPrefix(value, "/")) +} + +func writeHTTPAuthorizationError(w http.ResponseWriter, err error) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(newErrorResponse(err)) +} diff --git a/pkgs/ecmall/internal/server/authorization_test.go b/pkgs/ecmall/internal/server/authorization_test.go new file mode 100644 index 0000000..7271c0a --- /dev/null +++ b/pkgs/ecmall/internal/server/authorization_test.go @@ -0,0 +1,102 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "git.apinb.com/bsm-sdk/core/errcode" + "github.com/golang-jwt/jwt/v5" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const testAuthorizationKey = "0123456789abcdef0123456789abcdef" + +func TestHTTPAuthorization(t *testing.T) { + auth, err := newAuthorization(testAuthorizationKey, 3600, []string{"/passport.Login/Pwd", "/rest/fts/ping"}) + if err != nil { + t.Fatal(err) + } + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) + handler := auth.httpMiddleware(next) + + tests := []struct { + name string + path string + token string + want int + code int32 + }{ + {name: "grpc gateway anonymous", path: "/passport.Login/Pwd", want: http.StatusNoContent}, + {name: "dynamic rpc anonymous", path: "/rpc/passport/Login/Pwd", want: http.StatusNoContent}, + {name: "rest anonymous", path: "/rest/fts/ping", want: http.StatusNoContent}, + {name: "missing token", path: "/passport.Account/Get", want: http.StatusOK, code: int32(status.Code(errcode.ErrHeaderAuthorization))}, + {name: "valid raw token", path: "/passport.Account/Get", token: signedToken(t, time.Now(), time.Now().Add(time.Hour)), want: http.StatusNoContent}, + {name: "bearer rejected", path: "/passport.Account/Get", token: "Bearer " + signedToken(t, time.Now(), time.Now().Add(time.Hour)), want: http.StatusOK, code: int32(status.Code(errcode.ErrTokenAuthParseFail))}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, test.path, nil) + if test.token != "" { + request.Header.Set("Authorization", test.token) + } + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + if response.Code != test.want { + t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) + } + if test.want == http.StatusOK && !strings.Contains(response.Body.String(), `"code":`+fmt.Sprint(test.code)) { + t.Fatalf("unexpected authorization error: %s", response.Body.String()) + } + }) + } +} + +func TestAuthorizationRejectsTokenOlderThanConfiguredLifetime(t *testing.T) { + auth, err := newAuthorization(testAuthorizationKey, 60, nil) + if err != nil { + t.Fatal(err) + } + if err := auth.validate(signedToken(t, time.Now().Add(-2*time.Minute), time.Now().Add(time.Hour))); err == nil { + t.Fatal("expected token older than configured lifetime to be rejected") + } +} + +func TestGRPCAuthorization(t *testing.T) { + auth, err := newAuthorization(testAuthorizationKey, 3600, []string{"/passport.Login/Pwd"}) + if err != nil { + t.Fatal(err) + } + handler := func(context.Context, any) (any, error) { return "ok", nil } + + if _, err := auth.unaryInterceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/passport.Login/Pwd"}, handler); err != nil { + t.Fatalf("anonymous method failed: %v", err) + } + if _, err := auth.unaryInterceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/passport.Account/Get"}, handler); status.Code(err) != status.Code(errcode.ErrHeaderAuthorization) { + t.Fatalf("expected unauthenticated, got %v", err) + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", signedToken(t, time.Now(), time.Now().Add(time.Hour)))) + if _, err := auth.unaryInterceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "/passport.Account/Get"}, handler); err != nil { + t.Fatalf("valid token failed: %v", err) + } +} + +func signedToken(t *testing.T, issuedAt, expiresAt time.Time) string { + t.Helper() + claims := jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(issuedAt), + NotBefore: jwt.NewNumericDate(issuedAt), + ExpiresAt: jwt.NewNumericDate(expiresAt), + } + value, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(testAuthorizationKey)) + if err != nil { + t.Fatal(err) + } + return value +} diff --git a/pkgs/ecmall/internal/server/dynamic.go b/pkgs/ecmall/internal/server/dynamic.go new file mode 100644 index 0000000..e3f28a4 --- /dev/null +++ b/pkgs/ecmall/internal/server/dynamic.go @@ -0,0 +1,194 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + + "git.apinb.com/bsm-sdk/core/errcode" + "github.com/gin-gonic/gin" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + reflectionv1 "google.golang.org/grpc/reflection/grpc_reflection_v1" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" +) + +const maxDynamicRPCBody = 4 << 20 + +type dynamicGateway struct { + conn *grpc.ClientConn + mu sync.RWMutex + cache map[string]protoreflect.MethodDescriptor +} + +type dynamicRPCResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} + +func newDynamicGateway(grpcAddr string) (*dynamicGateway, error) { + conn, err := grpc.NewClient(reflectionTarget(grpcAddr), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("create dynamic gRPC client: %w", err) + } + return &dynamicGateway{conn: conn, cache: make(map[string]protoreflect.MethodDescriptor)}, nil +} + +func reflectionTarget(addr string) string { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + if host == "" || host == "0.0.0.0" || host == "::" { + host = "127.0.0.1" + } + return net.JoinHostPort(host, port) +} + +func (g *dynamicGateway) Close() error { return g.conn.Close() } + +func (g *dynamicGateway) handle(c *gin.Context) { + moduleName := strings.TrimSpace(c.Param("module")) + serviceShortName := strings.TrimSpace(c.Param("service")) + methodName := strings.TrimSpace(c.Param("method")) + if moduleName == "" || serviceShortName == "" || methodName == "" { + writeDynamicError(c, errcode.String(errcode.ErrInvalidArgument, "path must be /rpc/{module}/{service}/{method}")) + return + } + serviceName := moduleName + "." + serviceShortName + descriptor, err := g.resolveMethod(c.Request.Context(), serviceName, methodName) + if err != nil { + writeDynamicError(c, err) + return + } + if descriptor.IsStreamingClient() || descriptor.IsStreamingServer() { + writeDynamicError(c, errcode.String(errcode.ErrUnimplemented, "streaming RPC methods are not supported")) + return + } + + body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxDynamicRPCBody+1)) + if err != nil { + writeDynamicError(c, errcode.String(errcode.ErrInvalidArgument, "read request body: "+err.Error())) + return + } + if len(body) > maxDynamicRPCBody { + writeDynamicError(c, errcode.String(errcode.ErrResourceExhausted, "request body exceeds 4 MiB")) + return + } + + request := dynamicpb.NewMessage(descriptor.Input()) + if err := (protojson.UnmarshalOptions{DiscardUnknown: false}).Unmarshal(body, request); err != nil { + writeDynamicError(c, errcode.String(errcode.ErrJsonUnmarshal, err.Error())) + return + } + response := dynamicpb.NewMessage(descriptor.Output()) + ctx := outgoingMetadata(c.Request) + grpcMethod := "/" + serviceName + "/" + methodName + if err := g.conn.Invoke(ctx, grpcMethod, request, response); err != nil { + writeDynamicError(c, err) + return + } + + data, err := (protojson.MarshalOptions{UseProtoNames: false}).Marshal(response) + if err != nil { + writeDynamicError(c, errcode.String(errcode.ErrJsonMarshal, err.Error())) + return + } + c.JSON(http.StatusOK, dynamicRPCResponse{Code: int32(codes.OK), Message: codes.OK.String(), Data: data}) +} + +func (g *dynamicGateway) resolveMethod(ctx context.Context, serviceName, methodName string) (protoreflect.MethodDescriptor, error) { + cacheKey := serviceName + "." + methodName + g.mu.RLock() + method := g.cache[cacheKey] + g.mu.RUnlock() + if method != nil { + return method, nil + } + + stream, err := reflectionv1.NewServerReflectionClient(g.conn).ServerReflectionInfo(ctx) + if err != nil { + return nil, errcode.String(errcode.ErrUnavailable, "open gRPC reflection stream: "+err.Error()) + } + if err := stream.Send(&reflectionv1.ServerReflectionRequest{ + MessageRequest: &reflectionv1.ServerReflectionRequest_FileContainingSymbol{FileContainingSymbol: serviceName}, + }); err != nil { + return nil, errcode.String(errcode.ErrUnavailable, "query gRPC reflection: "+err.Error()) + } + reflectionResponse, err := stream.Recv() + if err != nil { + return nil, errcode.String(errcode.ErrUnavailable, "read gRPC reflection response: "+err.Error()) + } + fileResponse := reflectionResponse.GetFileDescriptorResponse() + if fileResponse == nil { + if reflectionErr := reflectionResponse.GetErrorResponse(); reflectionErr != nil { + return nil, sdkError(status.Error(codes.Code(reflectionErr.ErrorCode), reflectionErr.ErrorMessage)) + } + return nil, errcode.String(errcode.ErrRecordNotFound, "service descriptor not found") + } + + set := &descriptorpb.FileDescriptorSet{} + for _, encoded := range fileResponse.FileDescriptorProto { + file := &descriptorpb.FileDescriptorProto{} + if err := proto.Unmarshal(encoded, file); err != nil { + return nil, errcode.String(errcode.ErrInternal, "decode reflected descriptor: "+err.Error()) + } + set.File = append(set.File, file) + } + files, err := protodesc.NewFiles(set) + if err != nil { + return nil, errcode.String(errcode.ErrInternal, "build reflected descriptors: "+err.Error()) + } + descriptor, err := files.FindDescriptorByName(protoreflect.FullName(serviceName)) + if err != nil { + if err == protoregistry.NotFound { + return nil, errcode.String(errcode.ErrRecordNotFound, "service not found") + } + return nil, errcode.String(errcode.ErrInternal, "resolve service descriptor: "+err.Error()) + } + service, ok := descriptor.(protoreflect.ServiceDescriptor) + if !ok { + return nil, errcode.String(errcode.ErrRecordNotFound, "symbol is not a gRPC service") + } + method = service.Methods().ByName(protoreflect.Name(methodName)) + if method == nil { + return nil, errcode.String(errcode.ErrRecordNotFound, "method not found") + } + g.mu.Lock() + g.cache[cacheKey] = method + g.mu.Unlock() + return method, nil +} + +func outgoingMetadata(request *http.Request) context.Context { + pairs := make([]string, 0) + for name, values := range request.Header { + lower := strings.ToLower(name) + if lower != "authorization" && lower != "x-request-id" && !strings.HasPrefix(lower, "x-") { + continue + } + for _, value := range values { + pairs = append(pairs, lower, value) + } + } + return metadata.NewOutgoingContext(request.Context(), metadata.Pairs(pairs...)) +} + +func writeDynamicError(c *gin.Context, err error) { + c.JSON(http.StatusOK, newErrorResponse(sdkError(err))) +} diff --git a/pkgs/ecmall/internal/server/dynamic_test.go b/pkgs/ecmall/internal/server/dynamic_test.go new file mode 100644 index 0000000..8ab054c --- /dev/null +++ b/pkgs/ecmall/internal/server/dynamic_test.go @@ -0,0 +1,76 @@ +package server + +import ( + "encoding/json" + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/reflection" +) + +func TestDynamicGatewayInvokesUnaryRPC(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + grpcServer := grpc.NewServer() + healthServer := health.NewServer() + healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) + healthpb.RegisterHealthServer(grpcServer, healthServer) + reflection.Register(grpcServer) + go func() { _ = grpcServer.Serve(listener) }() + t.Cleanup(func() { + grpcServer.Stop() + _ = listener.Close() + }) + + gateway, err := newDynamicGateway(listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = gateway.Close() }) + + gin.SetMode(gin.TestMode) + engine := gin.New() + engine.POST("/rpc/:module/:service/:method", gateway.handle) + request := httptest.NewRequest(http.MethodPost, "/rpc/grpc.health.v1/Health/Check", strings.NewReader(`{"service":""}`)) + response := httptest.NewRecorder() + engine.ServeHTTP(response, request) + + if response.Code != http.StatusOK { + t.Fatalf("unexpected HTTP status: %d", response.Code) + } + var payload struct { + Code int32 `json:"code"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + if payload.Code != 0 || !strings.Contains(string(payload.Data), `"SERVING"`) { + t.Fatalf("unexpected response: %s", response.Body.String()) + } +} + +func TestOutgoingMetadataFiltersHeaders(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, "/", nil) + request.Header.Set("Authorization", "Bearer token") + request.Header.Set("X-Request-ID", "request-id") + request.Header.Set("Content-Type", "application/json") + ctx := outgoingMetadata(request) + forwarded, ok := metadata.FromOutgoingContext(ctx) + if !ok || len(forwarded.Get("authorization")) != 1 || len(forwarded.Get("x-request-id")) != 1 { + t.Fatalf("expected forwarded metadata: %v", forwarded) + } + if len(forwarded.Get("content-type")) != 0 { + t.Fatalf("content-type must not be forwarded: %v", forwarded) + } +} diff --git a/pkgs/ecmall/internal/server/response.go b/pkgs/ecmall/internal/server/response.go new file mode 100644 index 0000000..aa1e7b5 --- /dev/null +++ b/pkgs/ecmall/internal/server/response.go @@ -0,0 +1,78 @@ +package server + +import ( + "time" + + "git.apinb.com/bsm-sdk/core/errcode" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type errorResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` + Details any `json:"details"` + Timeseq int64 `json:"timeseq"` +} + +func sdkError(err error) error { + code := status.Code(err) + // SDK business errors already use custom codes and must pass through. + if code > codes.Unauthenticated { + return err + } + message := status.Convert(err).Message() + var target error + switch code { + case codes.Canceled: + target = errcode.ErrCanceled + case codes.InvalidArgument: + target = errcode.ErrInvalidArgument + case codes.DeadlineExceeded: + target = errcode.ErrDeadlineExceeded + case codes.NotFound: + target = errcode.ErrRecordNotFound + case codes.AlreadyExists: + target = errcode.ErrAlreadyExists + case codes.PermissionDenied: + target = errcode.ErrPermissionDenied + case codes.ResourceExhausted: + target = errcode.ErrResourceExhausted + case codes.FailedPrecondition: + target = errcode.ErrFailedPrecondition + case codes.Aborted: + target = errcode.ErrAborted + case codes.OutOfRange: + target = errcode.ErrOutOfRange + case codes.Unimplemented: + target = errcode.ErrUnimplemented + case codes.Unavailable: + target = errcode.ErrUnavailable + case codes.DataLoss: + target = errcode.ErrDataLoss + case codes.Unauthenticated: + target = errcode.ErrUnauthenticated + case codes.Internal: + target = errcode.ErrInternal + default: + target = errcode.ErrUnknown + } + if message == "" || message == status.Convert(target).Message() { + return target + } + return errcode.String(target, message) +} + +func newErrorResponse(err error) errorResponse { + response := errorResponse{ + Code: 500, + Message: err.Error(), + Details: "", + Timeseq: time.Now().UnixMilli(), + } + if grpcStatus, ok := status.FromError(err); ok { + response.Code = int32(grpcStatus.Code()) + response.Message = grpcStatus.Message() + } + return response +} diff --git a/pkgs/ecmall/internal/server/server.go b/pkgs/ecmall/internal/server/server.go new file mode 100644 index 0000000..5efd8ad --- /dev/null +++ b/pkgs/ecmall/internal/server/server.go @@ -0,0 +1,133 @@ +package server + +import ( + "bytes" + "context" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/gin-gonic/gin" + gwRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +type Server struct { + GRPC *grpc.Server + Gateway *gwRuntime.ServeMux + HTTP *gin.Engine + http *http.Server + dynamic *dynamicGateway + auth *authorization +} + +func New(key string, expireSeconds int64, anonymous []string) (*Server, error) { + auth, err := newAuthorization(key, expireSeconds, anonymous) + if err != nil { + return nil, err + } + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(auth.unaryInterceptor)) + reflection.Register(grpcServer) + engine := gin.New() + engine.Use(gin.Logger(), gin.Recovery()) + return &Server{ + GRPC: grpcServer, + Gateway: gwRuntime.NewServeMux(), + HTTP: engine, + auth: auth, + }, nil +} + +func (s *Server) Start(grpcAddr, httpAddr string) error { + grpcListener, err := net.Listen("tcp", grpcAddr) + if err != nil { + return fmt.Errorf("listen gRPC on %s: %w", grpcAddr, err) + } + httpListener, err := net.Listen("tcp", httpAddr) + if err != nil { + _ = grpcListener.Close() + return fmt.Errorf("listen HTTP on %s: %w", httpAddr, err) + } + + s.dynamic, err = newDynamicGateway(grpcAddr) + if err != nil { + _ = grpcListener.Close() + _ = httpListener.Close() + return err + } + s.HTTP.POST("/rpc/:module/:service/:method", s.dynamic.handle) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorder := newBufferedResponse() + s.Gateway.ServeHTTP(recorder, r) + if recorder.status != http.StatusNotFound { + recorder.flush(w) + return + } + s.HTTP.ServeHTTP(w, r) + }) + s.http = &http.Server{ + Addr: httpAddr, + Handler: s.auth.httpMiddleware(handler), + ReadHeaderTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 20, + } + fmt.Printf("ecmall gRPC services listening on %s\n", grpcAddr) + fmt.Printf("ecmall HTTP services listening on %s\n", httpAddr) + + errCh := make(chan error, 2) + go func() { errCh <- s.GRPC.Serve(grpcListener) }() + go func() { errCh <- s.http.Serve(httpListener) }() + serveErr := <-errCh + if errors.Is(serveErr, grpc.ErrServerStopped) || errors.Is(serveErr, http.ErrServerClosed) { + return http.ErrServerClosed + } + return serveErr +} + +func (s *Server) Stop(ctx context.Context) error { + stopped := make(chan struct{}) + go func() { + s.GRPC.GracefulStop() + close(stopped) + }() + select { + case <-stopped: + case <-ctx.Done(): + s.GRPC.Stop() + } + if s.dynamic != nil { + _ = s.dynamic.Close() + } + if s.http == nil { + return nil + } + return s.http.Shutdown(ctx) +} + +type bufferedResponse struct { + header http.Header + body bytes.Buffer + status int +} + +func newBufferedResponse() *bufferedResponse { + return &bufferedResponse{header: make(http.Header), status: http.StatusOK} +} + +func (r *bufferedResponse) Header() http.Header { return r.header } +func (r *bufferedResponse) WriteHeader(status int) { r.status = status } +func (r *bufferedResponse) Write(data []byte) (int, error) { return r.body.Write(data) } +func (r *bufferedResponse) flush(w http.ResponseWriter) { + for key, values := range r.header { + for _, value := range values { + w.Header().Add(key, value) + } + } + w.WriteHeader(r.status) + _, _ = w.Write(r.body.Bytes()) +} diff --git a/pkgs/ecmall/internal/server/server_test.go b/pkgs/ecmall/internal/server/server_test.go new file mode 100644 index 0000000..136e51e --- /dev/null +++ b/pkgs/ecmall/internal/server/server_test.go @@ -0,0 +1,24 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestHTTPRouterIsAvailable(t *testing.T) { + srv, err := New("0123456789abcdef0123456789abcdef", 3600, []string{"/healthz"}) + if err != nil { + t.Fatal(err) + } + srv.HTTP.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusNoContent) }) + + request := httptest.NewRequest(http.MethodGet, "/healthz", nil) + response := httptest.NewRecorder() + srv.HTTP.ServeHTTP(response, request) + if response.Code != http.StatusNoContent { + t.Fatalf("unexpected status: %d", response.Code) + } +} diff --git a/pkgs/ecmall/internal/service/address.go b/pkgs/ecmall/internal/service/address.go new file mode 100644 index 0000000..57307e5 --- /dev/null +++ b/pkgs/ecmall/internal/service/address.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/ec/address/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeAddress(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/ads.go b/pkgs/ecmall/internal/service/ads.go new file mode 100644 index 0000000..e957cb0 --- /dev/null +++ b/pkgs/ecmall/internal/service/ads.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/base/ads/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeAds(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/cms.go b/pkgs/ecmall/internal/service/cms.go new file mode 100644 index 0000000..ce4b5cb --- /dev/null +++ b/pkgs/ecmall/internal/service/cms.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/base/cms/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeCMS(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/feedback.go b/pkgs/ecmall/internal/service/feedback.go new file mode 100644 index 0000000..b32f4e6 --- /dev/null +++ b/pkgs/ecmall/internal/service/feedback.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/base/feedback/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeFeedback(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/fts.go b/pkgs/ecmall/internal/service/fts.go new file mode 100644 index 0000000..fc8e745 --- /dev/null +++ b/pkgs/ecmall/internal/service/fts.go @@ -0,0 +1,16 @@ +package service + +import ( + moduleService "bsm/full/module/base/fts/service" + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeFTS(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{Redis: impl.RedisService, Etcd: impl.EtcdService, DB: impl.DBService, Cache: impl.MemoryService}, + Engine: srv.HTTP, + Config: config.Spec.Fts, + }) +} diff --git a/pkgs/ecmall/internal/service/initial.go b/pkgs/ecmall/internal/service/initial.go new file mode 100644 index 0000000..ea79514 --- /dev/null +++ b/pkgs/ecmall/internal/service/initial.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/base/initial/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeInitial(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/logs.go b/pkgs/ecmall/internal/service/logs.go new file mode 100644 index 0000000..389e99a --- /dev/null +++ b/pkgs/ecmall/internal/service/logs.go @@ -0,0 +1,14 @@ +package service + +import ( + moduleService "bsm/full/module/base/logs/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeLogs(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{Redis: impl.RedisService, Etcd: impl.EtcdService, DB: impl.DBService, Cache: impl.MemoryService}, + Engine: srv.HTTP, + }) +} diff --git a/pkgs/ecmall/internal/service/mall.go b/pkgs/ecmall/internal/service/mall.go new file mode 100644 index 0000000..aa4f475 --- /dev/null +++ b/pkgs/ecmall/internal/service/mall.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/ec/mall/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeMall(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/market.go b/pkgs/ecmall/internal/service/market.go new file mode 100644 index 0000000..c7cf6bc --- /dev/null +++ b/pkgs/ecmall/internal/service/market.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/ec/market/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeMarket(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/mgt.go b/pkgs/ecmall/internal/service/mgt.go new file mode 100644 index 0000000..256c503 --- /dev/null +++ b/pkgs/ecmall/internal/service/mgt.go @@ -0,0 +1,16 @@ +package service + +import ( + moduleService "bsm/full/module/base/mgt/service" + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeMgt(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{Redis: impl.RedisService, Etcd: impl.EtcdService, DB: impl.DBService, Cache: impl.MemoryService}, + Engine: srv.HTTP, + Config: config.Spec.Mgt, + }) +} diff --git a/pkgs/ecmall/internal/service/order.go b/pkgs/ecmall/internal/service/order.go new file mode 100644 index 0000000..992750c --- /dev/null +++ b/pkgs/ecmall/internal/service/order.go @@ -0,0 +1,20 @@ +package service + +import ( + moduleService "bsm/full/module/ec/order/service" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeOrder(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + }) +} diff --git a/pkgs/ecmall/internal/service/passport.go b/pkgs/ecmall/internal/service/passport.go new file mode 100644 index 0000000..93c6eb0 --- /dev/null +++ b/pkgs/ecmall/internal/service/passport.go @@ -0,0 +1,22 @@ +package service + +import ( + moduleService "bsm/full/module/base/passport/service" + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposePassport(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + Config: config.Spec.Passport, + }) +} diff --git a/pkgs/ecmall/internal/service/sender.go b/pkgs/ecmall/internal/service/sender.go new file mode 100644 index 0000000..8c14a4e --- /dev/null +++ b/pkgs/ecmall/internal/service/sender.go @@ -0,0 +1,22 @@ +package service + +import ( + moduleService "bsm/full/module/base/sender/service" + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeSender(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + Config: config.Spec.Sender, + }) +} diff --git a/pkgs/ecmall/internal/service/service.go b/pkgs/ecmall/internal/service/service.go new file mode 100644 index 0000000..386fcdc --- /dev/null +++ b/pkgs/ecmall/internal/service/service.go @@ -0,0 +1,45 @@ +package service + +import ( + "fmt" + + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/server" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" +) + +type exposeFunc func(*server.Server) error + +var services = []struct { + name string + expose exposeFunc +}{ + {"ads", exposeAds}, + {"cms", exposeCMS}, + {"feedback", exposeFeedback}, + {"fts", exposeFTS}, + {"initial", exposeInitial}, + {"logs", exposeLogs}, + {"mgt", exposeMgt}, + {"passport", exposePassport}, + {"sender", exposeSender}, + {"address", exposeAddress}, + {"mall", exposeMall}, + {"market", exposeMarket}, + {"order", exposeOrder}, + {"wallet", exposeWallet}, +} + +func Expose(srv *server.Server) error { + srv.HTTP.Use(sessions.Sessions("mysession", cookie.NewStore([]byte(config.Spec.SecretKey+"-session")))) + for _, item := range services { + if !config.Enabled(item.name) { + continue + } + if err := item.expose(srv); err != nil { + return fmt.Errorf("expose %s: %w", item.name, err) + } + } + return nil +} diff --git a/pkgs/ecmall/internal/service/service_test.go b/pkgs/ecmall/internal/service/service_test.go new file mode 100644 index 0000000..353ca69 --- /dev/null +++ b/pkgs/ecmall/internal/service/service_test.go @@ -0,0 +1,62 @@ +package service + +import ( + "strings" + "testing" + + "bsm/full/pkgs/ecmall/internal/config" + ecmallserver "bsm/full/pkgs/ecmall/internal/server" +) + +// TestExposeRegistersPlatformModules guards the aggregate registry: the +// platform modules (ads, cms, feedback, fts, logs, mgt) must contribute their +// gRPC services and their native REST routes through service.Expose, and the +// gRPC-only modules must keep contributing theirs. +func TestExposeRegistersPlatformModules(t *testing.T) { + t.Setenv("BSM_SERVICES", "") + config.Spec.Services = []string{"all"} + + srv, err := ecmallserver.New("0123456789abcdef0123456789abcdef", 3600, nil) + if err != nil { + t.Fatalf("new server: %v", err) + } + if err := Expose(srv); err != nil { + t.Fatalf("expose: %v", err) + } + + services := make(map[string]bool) + for name := range srv.GRPC.GetServiceInfo() { + services[name] = true + } + for _, prefix := range []string{"ads.", "cms.", "feedback.", "address.", "mall.", "market.", "order.", "passport.", "sender.", "wallet.", "initial."} { + if !hasPrefix(services, prefix) { + t.Errorf("gRPC service with prefix %q is not registered", prefix) + } + } + + routes := make(map[string]bool) + for _, route := range srv.HTTP.Routes() { + routes[route.Method+" "+route.Path] = true + } + for _, path := range []string{ + "GET /rest/fts/ping", + "GET /rest/fts/config", + "POST /rest/fts/uploader", + "GET /rest/logs/ping", + "GET /rest/mgt/ping", + "POST /rest/mgt/login", + } { + if !routes[path] { + t.Errorf("native REST route %q is not registered", path) + } + } +} + +func hasPrefix(names map[string]bool, prefix string) bool { + for name := range names { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} diff --git a/pkgs/ecmall/internal/service/wallet.go b/pkgs/ecmall/internal/service/wallet.go new file mode 100644 index 0000000..4003718 --- /dev/null +++ b/pkgs/ecmall/internal/service/wallet.go @@ -0,0 +1,22 @@ +package service + +import ( + moduleService "bsm/full/module/finance/wallet/service" + "bsm/full/pkgs/ecmall/internal/config" + "bsm/full/pkgs/ecmall/internal/impl" + "bsm/full/pkgs/ecmall/internal/server" +) + +func exposeWallet(srv *server.Server) error { + return moduleService.Expose(moduleService.ExposeOptions{ + Dependencies: moduleService.Dependencies{ + Redis: impl.RedisService, + Etcd: impl.EtcdService, + DB: impl.DBService, + Cache: impl.MemoryService, + }, + GRPC: srv.GRPC, + Gateway: srv.Gateway, + Config: config.Spec.Wallet, + }) +} diff --git a/scripts/verify-workspace.ps1 b/scripts/verify-workspace.ps1 index 8beaa28..dd4e50a 100644 --- a/scripts/verify-workspace.ps1 +++ b/scripts/verify-workspace.ps1 @@ -7,7 +7,7 @@ $ErrorActionPreference = "Stop" $workspaceRoot = Split-Path -Parent $PSScriptRoot Push-Location $workspaceRoot try { - $unformatted = @(gofmt -l all module) + $unformatted = @(gofmt -l pkgs module) if ($unformatted.Count -gt 0) { throw ("gofmt required:" + [Environment]::NewLine + ($unformatted -join [Environment]::NewLine)) } diff --git a/scripts/verify-workspace.sh b/scripts/verify-workspace.sh index d39f7ce..b105f41 100755 --- a/scripts/verify-workspace.sh +++ b/scripts/verify-workspace.sh @@ -7,7 +7,7 @@ readonly WORKSPACE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" cd -- "${WORKSPACE_ROOT}" -unformatted="$(gofmt -l all module)" +unformatted="$(gofmt -l pkgs module)" if [[ -n "${unformatted}" ]]; then printf 'error: gofmt required:\n%s\n' "${unformatted}" >&2 exit 1 diff --git a/wiki/README.md b/wiki/README.md index a6d3bb6..e9781ec 100644 --- a/wiki/README.md +++ b/wiki/README.md @@ -3,6 +3,7 @@ | 文档 | 内容 | | --- | --- | | [系统架构](architecture.md) | 模块边界、聚合入口、请求分流和依赖注入 | +| [电商聚合开发上下文](../pkgs/ecmall/AGENT.md) | pkgs/ecmall 的注册表、配置、跨模块耦合、业务链路、缺口清单和开发指引 | | [开发与运维](operations.md) | 配置、启动、验证、构建、发布和排障 | | [审计报告](audit-2026-08-10.md) | 已修复问题、剩余风险和后续优先级 | | [API 接入总览](api/00-overview.md) | gRPC、动态 HTTP RPC、grpc-gateway、鉴权和 JSON 规则 | diff --git a/wiki/api/00-overview.md b/wiki/api/00-overview.md index cc33477..e7ba12e 100644 --- a/wiki/api/00-overview.md +++ b/wiki/api/00-overview.md @@ -1,6 +1,8 @@ # API 接入总览 -本文档面向 Web、移动端和第三方客户端开发者。默认示例基于 `all` 聚合服务:gRPC 监听 `12000`,HTTP 监听 `12001`,实际地址以 `all/etc/_.yaml` 为准。 +本文档面向 Web、移动端和第三方客户端开发者。默认示例基于 `pkgs/all` 聚合服务:gRPC 监听 `12000`,HTTP 监听 `12001`,实际地址以 `pkgs/all/etc/_.yaml` 为准。 + +电商客户端可以改用 `pkgs/ecmall` 聚合服务:它注册除 cloud 外的 initial、passport、sender、ads、cms、feedback、fts、logs、mgt、address、mall、market、order、wallet,gRPC 监听 `12300`,HTTP 监听 `12301`,配置位于 `pkgs/ecmall/etc/`。本目录中除 cloud 以外的章节对该入口同样适用,其中 ads、cms、feedback 走 gRPC/gateway,fts、logs、mgt 为原生 REST。 ## 协议入口 @@ -70,7 +72,7 @@ Content-Type: application/json Authorization: ``` -请求头直接传递 JWT,不添加 `Bearer` 前缀。`all` 使用 `Authorization.Key` 验证 HS256 签名,并根据 token 的 `exp` 与 `Authorization.Expire` 校验有效期;是否匿名由 `Authorization.Anonymous` 配置决定。 +请求头直接传递 JWT,不添加 `Bearer` 前缀。`pkgs/all` 使用 `Authorization.Key` 验证 HS256 签名,并根据 token 的 `exp` 与 `Authorization.Expire` 校验有效期;是否匿名由 `Authorization.Anonymous` 配置决定。 ## 当前限制与风险 diff --git a/wiki/architecture.md b/wiki/architecture.md index 29a8b93..a8c0608 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -2,7 +2,7 @@ ## 模块边界 -仓库包含 20 个 Go module:module 下 19 个业务模块可独立运行,all 聚合其中 15 个 base、ec、finance 服务;social 三个模块暂未聚合。 +仓库包含 21 个 Go module:`module/` 下 18 个业务模块可独立运行,`pkgs/all` 聚合其中 15 个 base、ec、finance 服务,`pkgs/ecmall` 聚合其中 14 个 base、ec、finance 服务(不含 cloud);social 三个模块暂未聚合,`scripts/api-docgen` 是文档生成工具 module,不属于业务服务。 典型模块目录职责: @@ -13,13 +13,13 @@ | internal/impl | DB、Redis、etcd、内存缓存 | | internal/logic | 业务逻辑 | | internal/server | 服务构造与注册 | -| service | 对 all 公开的 Expose API | +| service | 对聚合宿主(`pkgs/all`、`pkgs/ecmall`)公开的 Expose API | | pb | protobuf 生成代码 | | etc | 配置样例 | -独立入口初始化自己的基础设施。聚合调用 Expose 时不重复初始化,而是注入 all 创建的共享对象。 +独立入口初始化自己的基础设施。聚合调用 Expose 时不重复初始化,而是注入 `pkgs/all` 创建的共享对象。 -## all 启动流程 +## pkgs/all 启动流程 1. 校验 --workspace 并读取 etc/_.yaml。 2. 初始化共享 DB、Redis、etcd 和内存缓存。 @@ -29,6 +29,20 @@ HTTP 请求先进入 grpc-gateway;gateway 返回 404 时回退到 Gin。Gin 提供 `/rpc/{package}/{service}/{method}` 动态 JSON-to-protobuf unary 调用,以及 `/rest/{module}/...` 原生 REST 路由。动态调用通过本机 gRPC 端口进入完整 gRPC 处理链。 +## pkgs/ecmall 启动流程 + +`pkgs/ecmall` 与 `pkgs/all` 共享同一套 server、鉴权、动态 RPC 与配置装配实现,启动流程完全一致,差别只在注册表、端口和专项配置: + +| 项目 | pkgs/all | pkgs/ecmall | +| --- | --- | --- | +| module 路径 | bsm/full/pkgs/all | bsm/full/pkgs/ecmall | +| 监听端口 | gRPC 12000 / HTTP 12001 | gRPC 12300 / HTTP 12301 | +| 注册服务 | 15 个(10 base + 4 ec + wallet) | 14 个(9 base + 4 ec + wallet,不含 cloud) | +| 专项配置 | Fts、Mgt、Passport、Sender、Wallet | Fts、Mgt、Passport、Sender、Wallet | +| 本地 replace | 15 个业务模块 | 14 个业务模块 | + +`ecmall` 的注册表包含除 cloud 外的全部 base、ec、finance 服务,未注册的 cloud 既不会暴露 gRPC/gateway 路由,也不会解析其专项配置。两个入口使用同一份 `module/` 代码,可同时运行。 + ## 依赖注入 ExposeOptions.Dependencies 可接收 RedisClient、etcd Client、gorm DB 和 Cache。参数非 nil 时覆盖模块 internal/impl 对象。address、market、order、mgt 的业务数据库访问已统一使用 internal/impl.DBService。 diff --git a/wiki/audit-2026-08-10.md b/wiki/audit-2026-08-10.md index f6fd4c4..7ae9a6e 100644 --- a/wiki/audit-2026-08-10.md +++ b/wiki/audit-2026-08-10.md @@ -2,7 +2,7 @@ ## 范围 -覆盖根 workspace、all、19 个业务模块、配置样例和维护脚本。执行结构盘点、敏感信息扫描、TODO/异常退出扫描和全部 module 测试。 +覆盖根 workspace、`pkgs/all`、19 个业务模块、配置样例和维护脚本。执行结构盘点、敏感信息扫描、TODO/异常退出扫描和全部 module 测试。 ## 已修复 @@ -28,7 +28,7 @@ market 和 mall 的账号创建、登录、改密及初始化密码已从 MD5 ### P1:reflection 与明文 h2c -all 默认启用 gRPC reflection,自身不终止 TLS。生产环境应在可信网络运行,由网关终止 TLS,并将 reflection 改为配置控制。 +`pkgs/all` 默认启用 gRPC reflection,自身不终止 TLS。生产环境应在可信网络运行,由网关终止 TLS,并将 reflection 改为配置控制。 ### P1:未实现接口 @@ -36,7 +36,7 @@ social、wallet、mall、cms 等仍有模板 TODO。编译成功不代表功能 ### P2:测试覆盖不足 -多数 package 没有测试。应优先覆盖认证、权限、支付、订单状态机、迁移和 all 请求分流。 +多数 package 没有测试。应优先覆盖认证、权限、支付、订单状态机、迁移和 `pkgs/all` 请求分流。 ### P2:迁移与启动耦合 diff --git a/wiki/operations.md b/wiki/operations.md index adbbe97..f3ece29 100644 --- a/wiki/operations.md +++ b/wiki/operations.md @@ -6,7 +6,7 @@ /etc/_.yaml -all 的 serviceKey 来自 --workspace,默认 default。BSM_RuntimeMode 默认 dev,BSM_SERVICES 可覆盖聚合服务列表。 +`pkgs/all` 与 `pkgs/ecmall` 的 serviceKey 来自 --workspace,默认 default。BSM_RuntimeMode 默认 dev,BSM_SERVICES 可覆盖聚合服务列表;`pkgs/ecmall` 的配置文件位于 `pkgs/ecmall/etc/`,默认监听 gRPC 12300 / HTTP 12301。 ## 验证 @@ -22,10 +22,13 @@ Linux/macOS: ## 构建 -scripts/build-all-linux.sh 构建 module 下独立服务到 .builds。all 可单独构建: +scripts/build-all-linux.sh 构建 module 下独立服务到 .builds。`pkgs/all` 和 `pkgs/ecmall` 可分别单独构建: - cd all - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o ../.builds/all ./cmd/main + cd pkgs/all + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o ../../.builds/all ./cmd/main + + cd pkgs/ecmall + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o ../../.builds/ecmall ./cmd/main ## 发布检查