Files
full/pkgs/ecmall/AGENT.md

65 KiB
Raw Permalink Blame History

pkgs/ecmall 开发上下文

本文面向需要在 pkgs/ecmall 或其关联子模块上继续开发的工程师,说明这个聚合入口是什么、代码怎么组织、请求怎么流转、数据放在哪里、哪些实现是缺口,以及改动时的注意事项。

其他文档的分工:模块职责概览见 根 README,聚合入口与请求分流的实现见 系统架构,配置/构建/发布流程见 开发与运维,逐接口的请求字段见 API 接入总览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 与动态 RPCftslogsmgt 三个模块只有原生 REST 路由/rest/fts/rest/logs/rest/mgt),不注册任何 gRPC 服务。专项配置节为 FtsMgtPassportSenderWalletgo.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/serverinternal/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/SIGTERMsrv.Stop(ctx),先 GracefulStop10 秒超时后强制 Stop,再关 HTTPcmd/main/main.go:46-57

3.2 配置解析规则

由 SDK conf.New 决定(D:\work\bsm-sdk\core\conf\new.go:25-67

<BSM_Prefix>/etc/<小写 serviceKey>_<BSM_RuntimeMode>.yaml
  • 文件不存在时回退到 <BSM_Prefix>/etc/workspace_<BSM_Workspace>_<Mode>.yamlnew.go:39-43
  • YAML 内容会经 os.ExpandEnv,支持 ${VAR} 替换(new.go:55
  • YAML 中必须包含 Service: 键,否则 log.Fatallnnew.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.NewAuthorization.Key 覆盖
BSM_Licence 许可文件路径

D:\work\bsm-sdk\core\env\env.go:15-35BSM_SERVICESinternal/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

adscmsfeedbacklogs 没有专项配置节:它们的 service 包不暴露 Config 类型,聚合无法向它们注入任何模块级配置,只能注入共享依赖。FtsMgt 两个节在 etc/default_dev.yaml 中必须存在,否则 config_test.go 直接失败。

ServicesBSM_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 达梦 DAMENGDriver: 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 独立时走达梦,feedbackmilu)。部署前必须确认目标库里这 14 个模块的表都在。

2聚合启动不会建表也不会跑 seed。

  • 聚合走 SDK with.Databases(cfg, nil)internal/impl/impl.go:38。SDK 仅在 options.IsAutoMigrate 为真时执行迁移,而该默认值是 falseD:\work\bsm-sdk\core\database\sql\postgresql.go:17D:\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 登记):adsads_item.go:38ads_pos.go:22)、cms9 个模型各自 init(),如 cms_accessory.go:22)、feedback3 个模型各自 init())、ftsfts_record.go:40)、logslog_data.go:24),以及 initialpassportsendermallwallet。由于上面那条默认值,这套登记在独立模式与聚合模式下都不会真正建表
  • 第二套(不设开关的 DBService.AutoMigrate(migrateTables...)addressmarketorder,以及 mgtmodule/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 上,聚合路径不执行。passportrelation 的这行甚至是注释掉的(module/base/passport/cmd/main/main.go:37module/social/relation/cmd/main/main.go:37)。

结论:表结构必须在启动 ecmall 之前由独立模式启动一次、迁移脚本或 DBA 建好mgt 的管理员账号也需要另行播种。


4. 请求链路与鉴权

4.1 四种入口ecmall 全都有

入口 形态 ecmall 是否可用
原生 gRPC /{package}.{Service}/{Method} 35 个服务
grpc-gateway POST /{package}.{Service}/{Method} 同 35 个服务
动态 RPC POST /rpc/{package}/{Service}/{Method} 对全部已注册的 gRPC 服务可用
原生 RESTGin /rest/{module}/... ftslogsmgt(约定入口)

/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/:methodserver.go:61)。

监听参数:ReadHeaderTimeout=10sIdleTimeout=120sMaxHeaderBytes=1MBserver.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 走 protojsondynamic.go:94-112
  • 只支持 unarystreaming 明确报错(dynamic.go:79-82
  • 请求体上限 4 MiBdynamic.go:30:89-92
  • 只透传 authorizationx-request-id 和所有 x- 前缀头到下游 gRPCdynamic.go:178-190
  • 需要本进程开启 gRPC reflectionserver.New 无条件 reflection.Registerserver.go:33

4.4 鉴权

internal/server/authorization.go

  • gRPCunary 拦截器,非匿名方法必须带 authorization metadataauthorization.go:42-53
  • HTTP中间件校验 Authorization 头(authorization.go:55-66
  • Token 为 HS256 JWT直接放在 Authorization 头,不加 Bearer(加前缀会被判为解析失败)
  • 校验项:签名算法必须 HS256、必须有 exp、必须有 iatiat 不能在将来、now - iat 不得超过配置的 Expireauthorization.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/Pwdpassport.Login/Codepassport.Login/Quick
  • passport.Register/Pwdpassport.Register/Code
  • passport.Forget/Verifypassport.Forget/Reset
  • passport.Verify/Requestpassport.Verify/JumioCallback
  • market.Agency/Loginmall.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/Sendsender.Sms/Verifysender.Mail/Send dev 配置里匿名 需要 JWT
feedback.Method/GetDeleteRemarkList 逻辑内部不鉴权、不校验归属§9.3 需要 JWT宿主拦截但通过后仍无归属校验
logs 的 POST /rest/logs/createfetchtotal 模块内 JWT 中间件被注释 ecmall 白名单列这三条 → 需要 JWT/rest/logs/ping 已列入白名单)

如果希望保持独立运行时的行为,必须显式加入白名单。

4.6 错误响应

统一为 {code, message, details, timeseq}HTTP 状态码恒为 200code 为 gRPC status codeinternal/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 CheckData gRPC + gateway
logs 仅原生 REST /rest/logs
mgt Mgt 仅原生 REST /rest/mgt
passport AccountForgetLoginRegisterVerify Passport gRPC + gateway
sender MailSms Sender gRPC + gateway
address Library gRPC + gateway
mall AdsCategoryFreightNoticeProductStaffStore gRPC + gateway
market AgencyDataSupply gRPC + gateway
order CartCouponMgtSummary gRPC + gateway
wallet AlipayBasicPaymentWechat Wallet gRPC + gateway

ftslogsmgt 三个模块的 ExposeOptions 只有 Dependencies + Engine*gin.Engine没有 GRPC/Gateway 字段,因此它们既不出现在原生 gRPC 面,也不出现在 gateway 与动态 RPC 面,只能通过 /rest/... 调用。

注册来源:模块的 service/expose.gogateway / RESTinternal/server/new.gogRPC。逐方法清单见 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-32services 表里增删一行;同时同步 internal/config/config_test.goecmallServices 期望值与 etc/default_dev.yamlServices 列表(该测试会强制三者一致)。internal/service/service_test.go 还会断言各模块的 gRPC/REST 面确实注册成功,漏接线会直接失败。


6. 依赖注入机制

每个模块的 service/expose.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_itemads_pos(后者空模型)
cms cms_accessorycms_categorycms_commentcms_pagescms_postcms_relate_categorycms_relate_tagscms_sitecms_tags
feedback feedback_accessoryfeedback_imagesfeedback_item
fts fts_record
initial initial_appsinitial_areasinitial_configinitial_countryinitial_datas
logs log_dataTableName(),按 GORM 默认名——推断)
mgt mgt_usersmgt_rolesmgt_departmentsmgt_applications(四者无 TableName(),用 GORM 复数默认名)、mgt_permissionmgt_link_user_dptmgt_link_user_rolemgt_link_user_pmnmgt_link_user_appmgt_link_role_pmnmgt_link_role_appmgt_link_dpt_rolemgt_link_dpt_pmn
passport passport_accountpassport_datapassport_notifypassport_providerTableName())、passport_rights_expirypassport_scorepassport_statisticspassport_tagspassport_verify
sender SenderTemplateTableName(),按 SDK 的 SingularTable 推断为 sender_template——推断)
address address_library
mall mall_adsmall_applymall_categorymall_freightmall_freight_attrmall_freight_denymall_noticemall_productmall_product_attrmall_product_commentmall_product_photosmall_product_specmall_staffmall_storeproduct_categoryproduct_spec
market market_agencymarket_supply
order order_cartorder_couponorder_detailsorder_summary
wallet wallet_apply_cashwallet_bankwallet_basicwallet_paymentwallet_recordwallet_refund

注意 mall 里有两个不带 mall_ 前缀的表:product_categoryproduct_specmall_product_link_category.go:20mall_product_link_spec.go:20)。

7.2 共享字段约定

跨模块关联不用数字外键,全部用字符串 identityD:\work\bsm-sdk\core\types\db.go:20-76

嵌入结构 字段 说明
Std_IDIdentity ididentity identity 为 varchar(36) 唯一索引24 位 NanoID 或 36 位 ULID
Std_IICUDS ididentitycreated_atupdated_atdeleted_atstatus 最常用;status 默认 0、-1 禁用、1 正常
Std_Passport passport_idpassport_identity 指向用户;跨模块引用用户就靠 passport_identity
Std_Owner owner_idowner_identity 资源归属
Std_Status status int64 仅状态

业务侧还有一组约定字段:store_identity(店铺)、product_identity(商品)、address_identity(收货地址)、order_no18 位订单号,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:68summary/quick_create_by_product.go:79mgt/order_create.go:101
order mall_product mall summary/submit.go:82:112summary/quick_create_by_product.go:51:53mgt/order_create.go:57
order mall_product_spec mall summary/submit.go:123:204:209summary/quick_create_by_product.go:68mgt/order_create.go:67:141mgt/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.Productmodels.SpecOrderAddress,见 module/ec/order/internal/models/query.go:3:16types.go:7没有 gorm 模型、没有编译期约束mall 改一个列名order 只会在运行时报错。

反向也要记住:addressmall(含 mall_staff)、marketorderwallet 的表都有外部读者,不能随意改列。新加入的 6 个平台模块只新增了上表这一条跨模块读取cms → mall_staff),彼此的 ads_*cms_*feedback_*fts_recordlog_datamgt_* 表都与其它模块无关。


8. 核心业务链路

8.1 下单主链路(Summary.Submit

module/ec/order/internal/logic/summary/submit.go:24-247

  1. 鉴权:service.ParseMetaCtx(ctx, &service.ParseOptions{RoleValue: "Mall_Admin"})如果请求带了内嵌 Address,角色校验被置空:26-33
  2. 读购物车:order_cart where passport_identity = <当前用户>:45
  3. 取收货地址:内嵌 Address 优先;否则读 address_library where identity = <AddressIdentity>:55-76
  4. 按店铺分组:对每个购物车项读 mall_productstore_identity,以 store_identity 为 key 分桶(:79-95
  5. 每个店铺生成 summaryIdentity = utils.UUID()orderNo = common.CreateOrderNo():103-104
  6. 店铺内逐件读 mall_producttitle/cover_image/cost_price/supply_id/args/gas_types)与 mall_product_spectitle/serial_number/price),累加金额(:112-135
  7. 组装 order_detailsorder_summary(写入地址快照Std_PassportStoreID/StoreIdentityTransPrice/TotalPriceStatus = 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 / ByChargemodule/finance/wallet/internal/logic/payment/),按 PayChannel1 微信 / 2 支付宝 / 3 钱包余额)分发,内部复用 wechat.goMiniPaySign/AppPaySign/NativePaySignalipay.goTradeWapPay/TradeAppPay,并写 wallet_payment
  • wallet.Payment.ByOrder 会通过 models/query.go:104 直接读 order_summary.trans_pricestatus=1)核对金额。
  • wallet.Alipay/* 四个 RPC 与 wallet.Wechat/AppPreOrderNativePreOrderTransfer 全是 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/senderpassport 引用的记录,也没有 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-26login.Codelogin.Quick 同样只判空。mall.Staff/Login 的验证码分支也不校验(module/ec/mall/internal/logic/staff/login.go:45-58)。
  • ⚠️ passport 的密码算法不统一:注册、改密、登录用 bcryptforget.Resetutils.Md5module/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 客户端;ftslogsmgtpb 都不存在)。它们的链路各自独立,只有下面这一处真正的跨模块耦合:

链路 走向
后台管理mgt POST /rest/mgt/loginpub.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),取不到就直接返回 ErrDBadd_comment.go:40 还对结果做无保护断言 → 非 mall 员工身份评论会失败/panic
反馈feedback Addfeedback_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_dataPOST /fetch/total 查询统计;模块内三条路由(含写入)都是匿名,但 ecmall 白名单只放行 /rest/logs/ping
广告ads 只读:ads.Fetch/ByPospos_key + statusads_item 并解析成展示项

结论:把这 6 个模块接进 ecmall得到的是同一个进程、同一套鉴权、同一个数据库,而不是一条新的业务闭环;它们之间的协作目前只存在于数据库层面。


9. 子模块速查

9.1 ads — 广告位内容分发(只读)

  • 暴露Fetch(1),唯一方法 ByPos;无 ConfiggRPC + gateway POST /ads.Fetch/ByPos
  • ads_itemads_posads_pos 除迁移登记外没有任何查询,属空模型)
  • 逻辑internal/logic/fetch(按 pos_key + status 查启用广告并解析成文本/图片/视频条目)
  • 端口:独立 dev 配置 gRPC 12216 / gateway 12102rst_devads_prod.yamlads_test.yaml 与 dev 逐字相同prod 仍指向 rst_dev + CHANGE_ME
  • 缺口
    • by_pos.go:21 直接解引用 impl.DBService,聚合未注入 DB 时首个请求 panic推断
    • 全量 FindLimit/分页,广告量大时一次性返回
    • 独立入口 cmd/main/main.go 从不调用 service.Expose,且 internal/server/new.go:26-30 从未给 Server.Mux 赋值 → 独立进程的 gateway 形同虚设(推断 404
    • impl.MemorySericeimpl.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 + gatewayPOST /cms.<Service>/<Method>
  • 9 张 —— cms_postcms_categorycms_tagscms_commentcms_pagescms_sitecms_accessorycms_relate_categorycms_relate_tags
  • 逻辑internal/logic/{post,pages,category,tags,site};鉴权靠每个 logic 方法内部调 service.ParseMetaCtx不在聚合层统一处理
  • 端口gRPC 12410 / gateway 12409独立 dev 库 ec_mall
  • 缺口
    • site 子域整体空壳却已注册:site/create.go:20-27modify.go:21-28delete.go:27-32 返回固定 OK 不写库;fetch.go:18-22get.go:24-26 返回零值 + nil error
    • 未鉴权写入category/modify.go 完全不调 ParseMetaCtx
    • 跳过鉴权的读:post/fetch.gopost/get_by_key.gopost/get_by_identity.gopost/search.gopages/{fetch,get_by_key,get_by_identity}.gocategory/fetch.gopost/comment_list.go:24ParseMetaCtx 被注释
    • panic/错算:post/add_comment.go:40 未检查的类型断言;post/search.go:31if pageSize < 50 判断反向、:43make(len(data)) 再 append结果翻倍且前半为 nilpost/comment_list.go:18-19 在钳制前取值
    • 可刷量:incr_post_like.go:33 要求 op_identity 却从不使用(models/query.go:346 TODO重复点赞不去重
    • 静默失败:models/query.go:36 忽略 Create 错误、:379 忽略 Count 错误、:375Find 代替 First(删除不存在的分类也报成功)
    • InitData() 无调用点 → _RootCategory 永不创建;生产代码开着 GORM .Debug()query.go:119 等)与 fmt.Println
    • 跨模块裸读 mall_staffpost/add_comment.go:35,见 §7.3
  • 测试:无(test/*.http 的路径已与生成的 /cms.<Service>/<Method> 脱节,实测会 404

9.3 feedback — 用户反馈工单

  • 暴露Method(6)List/Get/Add/Modify/Delete/RemarkConfiggRPC + gateway POST /feedback.Method/<Method>
  • feedback_itemfeedback_imagesfeedback_accessory
  • 逻辑internal/logic/method(单包)
  • 端口dev 只有单端口 ListenOn: 0.0.0.0:12210dev 库 milu
  • 缺口
    • Get/Delete/Remark 完全不调 ParseMetaCtxget.go:19-42delete.go:19-35remark.go:19-41):能读任意工单的 email/phone、删任意工单、改任意备注状态Modify 只解析身份不校验归属(modify.go:24
    • 假成功:Delete/Remark/Modify 不校验影响行数即返回 OKdelete.go:26-34remark.go:32-40Updates(struct) 忽略零值 → 无法置 status=0 或清空备注
    • modify.go:36-38 用新 utils.UUID() 覆盖 identityUpdates推断unique 键被改写,记录失联);modify.go:71-75 构造 FeedbackAccessory 未设 Identity(第二条起唯一索引冲突)
    • list.go:50 过滤列写 username,模型列名是 user_namefeedback_item.go:15)→ 带该条件即 SQL 报错;list.go:38-45agency 就跳过 passport_identity 过滤(且 agency 无写入点,筛选恒假)
    • 假级联:delete.go:25 注释称会自动删图片/附件,但模型没有 OnDelete,实际只软删主表 → 子表残留
    • 独立 yaml 用的是旧键(Name/ListenOn/Dsn),既无 Service: 也无 Databases: → 独立启动会在 conf.New 直接 fatal推断
  • 测试:无(test/rpc/rpc.go 全注释)

9.4 fts — 文件上传与存储

  • 暴露没有 gRPCExposeOptions 只有 Dependencies + EngineConfig 就是 internal/config.SrvConfig(含 MinioOssLocalFtsConfig
  • RESTGET /rest/fts/pingGET /rest/fts/config(匿名)、POST /rest/fts/uploader(模块自带 middleware.JwtAuth(true)
  • fts_record(只写不读,handler.go:101无跨模块表访问
  • 逻辑internal/logic(上传编排)+ internal/routers/uploader.go;存储走本地 Local.UploadDir 或 MinIO本地 URL 由 Local.Site + 年-月/identity前2位 拼成
  • 端口dev/test/prod 三份配置完全相同:单端口 16290dev 库 bsm_devLocal.Site 指向外部静态站 http://files.apinb.com
  • 缺口
    • 路径穿越面bucket 直接取自 c.PostFormhandler.go:27)未做任何校验,随后 filepath.Join(UploadDir, bucket, ...)provider.go:26,37)并进入返回 URLprovider.go:71)——../ 可逃出 UploadDir
    • 上传校验薄弱:只按文件名后缀白名单且区分大小写(handler.go:63-64,127-133),无 MIME/魔数校验;大小检查发生在 multipart 完全解析之后(handler.go:49-57
    • provider.go:115identity[0:2]claims.Identity 短于 2 字节即切片越界(被 gin.Recovery 兜成 500
    • nil 解引用:FtsConfig/Local/MinioOss 三者都不校验(config.go:40 只查 ServiceCache),缺失时 handler.go:49,57,128provider.go:26,71,79 panic
    • record.Status 硬编码 0、HandleCmd/HandleArgs 永不写入、算好的 Hash 没有秒传/去重实现;文件已落盘后才因 chmod 失败返回错误(provider.go:63-67
    • 死代码:internal/logic/fetch.go 整段注释、models/query.go:4-5InitData()internal/errorsinternal/response262 行)无导入方
    • 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 — 客户端初始化与基础字典

  • 暴露CheckHello/Config/UpdatesDataCountry/Areas/DatasExposeOptions Config
  • initial_appsinitial_areasinitial_configinitial_countryinitial_datas
  • 逻辑internal/logic/check(配置与版本)、internal/logic/data(字典),各带 Redis 缓存封装(键 configcountryareasdatas
  • 端口gRPC 12101 / gateway 12102
  • 缺口data/areas_cache.go:33-34enabled = ?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
  • RESTGET /rest/logs/pingPOST /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/helloping
  • 端口dev 配置 Service: oplogs、单端口 16289、Driver: dm达梦database=DAMENGlogs_test.yaml 换成 postgres 的 rst_devprod 与 dev 端口一致
  • 缺口
    • 分页参数从不读取fetch.go:33-34,48-53)→ 永恒 page=1/size=50客户端翻页静默失效
    • fetch.go:41-42 条件键写 op_ip 却读 request["ip"],缺 ip 时对 nil 断言 panicfetch.go:36,39,45 是无保护类型断言JSON 数字解成 float64→ panic
    • total.go:28Group + 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-20Hmac/Encry 从不使用或校验
    • cmd/cli/main.go 是死代码(路径过期、HttpPost 错误被忽略);fetch.go:36,39total.go:24tx.Where 未回写GORM 首次链式后原地改动,能用但脆弱——推断)
  • 测试:无(test/*.http 仍指向 /oplogs/v1/*

9.7 mgt — 后台管理RBAC 底座)

  • 暴露没有 gRPCExposeOptions 只有 Dependencies + EngineConfigMgt 节(InitRootUserSecretKey
  • REST/rest/mgt 下共 63 条路由
    • 匿名 5 条:GET /pingGET /sessionPOST /loginPOST /refreshPOST /resetinternal/routers/register.go:31-36
    • /user 15 条4 条本人可查(role/app/pmn/pmn_tree,仅需 JWT+ 11 条 admincreate/del/detail/modify/fetch/list/set_role/del_role/set_pmn/modify_pmn/del_pmn
    • /app 8、/role 12、/pmn 8、/dpt 15全部 JWT + admin
    • 中间件SDK middleware.JwtAuth(true) + 模块自己的 internal/middleware/rbac.goRequireAdmin:39IsSuperAdmin:19EnsureSelfOrAdmin:57
  • 13 张 —— mgt_usersmgt_rolesmgt_departmentsmgt_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_dev127.0.0.1:19432、Redis 19379/0prod 库 factor_prod
  • 缺口
    • 聚合不建表也不播种 rootAutoMigrateInitRootUserData() 只在 models.New 里执行,而它挂在 impl.NewImpl()cmd/main 上;聚合只调 service.Expose(见 §3.5)。InitRootUser 的注释/README 说「未配置默认 true」实现是 nil → falseinternal/config/config.go:19-27),且 mgt_prod.yaml/mgt_test.yaml 都没有该键 → 生产默认关闭
    • 匿名写端点POST /rest/mgt/reset 无鉴权即可改密(仅靠 Redis 短信码,无频率限制);GET /rest/mgt/session 匿名写 session聚合的 service.Expose 已注册 mysession storeinternal/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) 的关联行
    • IsSuperAdminaccount == "root" 或硬编码中文角色名「超级管理员」判定(rbac.go:14-15,24,31DB 出错一律返回 false
    • nil 解引用:impl.RedisService/impl.DBService 未被注入时 pub/forget.go:58rbac.go:21 panicSecretKey 字段没有 yaml tag实际值来自 BSM_JwtSecretKeyinternal/config/config.go:39
    • 死代码:builtinManagementMenuTemplates()ForgetPwdByPwd(空函数且未注册)、SaveToken 均无调用点
  • 测试internal/routers/register_test.go(本机通过)

9.8 passport — 用户账号与身份认证

  • 暴露AccountGet/SetData/SetPassword/TagCreate/TagRemove/StatisticsForgetVerify/ResetLoginPwd/Code/QuickRegisterPwd/CodeVerifyRequest/JumioCallbackConfigPassport 节:TokenKycWeChatConf
  • 9 张 passport_*
  • 逻辑accountcommonJWT 生成)、forgetloginregisterverifyJumio 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.Verifylogin.Codelogin.Quick 不比对验证码
    • account.Statisticslogin_count 统计的是账号行数(最多 1不是登录次数
    • models/cache.go 整套缓存/验证码/登录尝试 API 与 query.CreateAccount 无任何调用方(死代码)
    • WeChatConf 只有 json tag 没有 yaml tagconfig.Spec.WeChat 在代码中未被使用
  • 测试:无(仅 test/grpc/main.go 手工客户端,硬编码 api.apinb.com:10020

9.9 sender — 短信与邮件

  • 暴露MailSendSmsSend/VerifyConfigSMTP 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, nilinternal/logic/sms/send.go:144-146),调用方随后 json.Marshal(nil) 得到 "null" 当成功返回
    • 邮件只实现了 qq 渠道,其它 provider 返回 ErrNotProviderinternal/logic/mail/send.go:55-60impl.NewSMTP 直接返回 nilProvider.Google/Provider.QQ 是死字段
    • 日发送量限制失效:limitKeyGet 从不写回(send.go:42-51),计数永不增长
    • 黑名单只读不写:仓库内没有写 /SMS/BlackList/ 的代码;配置项 Code.BlackListFilterCode.GenerateCodeCode.CokeyKey 均无读取点
    • sms/verify.go:34-35比对失败分支删键,成功反而保留验证码
    • config.Spec.Code 无 nil 校验即解引用(send.go:49 等)
  • 测试:仅两个 //go:build integration 的联网测试(test/grpc/mail_test.gosms_test.go),默认 go test 不执行

9.10 address — 收货地址簿

  • 暴露LibraryCreate/Modify/Get/Fetch/DeleteConfig
  • address_library
  • 逻辑internal/logic/library(唯一子包)
  • 端口gRPC 12460 / gateway 12459独立 dev 库为 ec_mall
  • 注意etc/address_dev.yamlService: 写成了 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/passwordbcrypt+ internal/excode
  • 端口gRPC 12420 / gateway 12419独立 dev 库为 ec_mall
  • 缺口(多为「返回成功但不做事」):
    • freightcreatemodifydeletedeny_region_createdeny_region_deletedeny_region_modifydeny_region_remove 全部返回固定 OK 不写库;detailfetchdeny_region_fetch 是裸 returnnil, nil
    • store/apply_joinstore/mini_code 空实现;store/licensing 只查店铺后返回 OK
    • product/item_detail_by_spec.go:16-18// TODO: valid code + 裸 return
    • 孤儿函数:freight/remove.gofreight/deny_region_remove.go 有真实实现但没有被接线server 映射到的是空实现版本
    • staff/login.go:45-58 的验证码分支不校验验证码即签发 token非 1/2 的 LoginGenre 返回 ErrUnimplemented
    • item_fetch.go:28 声明并用于 SQL 的 idList 从未 appendstore/search.go:39keyword 过滤但列名是 keywords
    • product/item_detail.go:31 跨模块读 market_supply
  • 测试:仅 internal/password/password_test.go

9.12 market — 代理与供应商

  • 暴露Agency(9)、Supply(5)、Data(5);无 Config
  • market_agencymarket_supply
  • 逻辑internal/logic/agency(省代账号全生命周期,含审核)、internal/logic/supplyinternal/logic/data(看板)
  • 端口gRPC 12424 / gateway 12423独立 dev 库为 ec_mall
  • 缺口
    • logic/data 下 5 个方法Overview/MemberFetch/MemberDetails/OrderFetch/OrderDetails全部只有 TODO + 裸 return,返回 (nil, nil)
    • agency/fetch.go:35agency/pending.go:36 写成 tx.Where(...) 未回写 txidentity 过滤很可能不生效
    • agency/modify.go:53supply/modify.go:46 返回的 Identity 从未赋值,恒为空串
    • agency/create.go:20-23delete.go:17-21supply/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_cartorder_couponorder_detailsorder_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-117NULL 或字段缺失会 panic配送员查询整段被注释
    • cart/fetch.go:40-48 原生 SQL 出错时静默丢数据(只在 err == nil 时追加结果,不记录也不返回)
    • coupon/by_status.go:52Intro 赋给 Amount;入参 Status 未被使用
    • 死代码:models/query.go:22 QuicklyCreateOrderorder_summary.go:70 GetSummaryCnt:83 GetSummaryList(后者引用了模型中不存在的列 buyer_identity/merchant_identity/pay_status 等)
    • Simulate* 见 §8.2
  • 测试test/rpc/ 下 4 个联网集成测试(cart_test.gocoupon_test.gomgt_test.gosummary_test.go)都带 //go:build integration,默认 go test 不编译不执行;同目录 bisic_test.go 没有 build tag但也不含任何 Test* 函数(只定义 GetCoon() 辅助函数,内硬编码 127.0.0.1:12442BSM_ORDER_TOKEN),所以默认 go test ./... 对这个包只是编译通过、不跑任何用例

9.14 wallet — 钱包与支付

  • 暴露Alipay(4)、Basic(8)、Payment(6)、Wechat(5)ConfigWeChatAlipayWalletQrCodeSavePath
  • 6 张 wallet_*
  • 逻辑internal/logic/basic(钱包、支付密码、银行卡、提现)、internal/logic/payment(渠道编排)、internal/logic/wechatinternal/logic/alipay
  • 端口gRPC 12238 / gateway 12239
  • 缺口
    • Alipay.WapPay/PagePay/AppPay/TransferWechat.AppPreOrder/NativePreOrder/Transfer 全是 TODO(裸 return);只有 Wechat.JsapiPreOrder 真正实现
    • Wechat.WxCallback 不落库不改状态
    • jsapi_pre_order.go:22wx_callback.go:26 丢弃 NewWechat() 的错误,失败时会 nil 解引用
    • wechat.go:48WeChat.PrivateKey 配置值当文件路径读取
    • config.Spec.Alipay/Walletetc/wallet_dev.yaml根本没有这两个节,指针为 nil相关分支依赖调用时机
    • wallet_test.yaml/wallet_prod.yaml 用的是旧键名(Name/ListenOn/Dsn),与当前 SrvConfig 不匹配
    • WalletRefund 只有模型定义无任何读写;ChargeWallet 无调用方
    • 跨模块读 order_summarypassport_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-26login/code.gologin/quick.gomall/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/createdeletesupply/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-27internal/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.gopost/{fetch,search}.go
mgt InitRootUser 注释与实现相反(nil → falseprod 配置缺该键 → 管理员不播种 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_joinmini_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-18freight/remove.gofreight/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-146mail/send.go:55-60
cms site 5 个方法空壳3 个假成功 + 2 个返回零值) module/base/cms/internal/logic/site/*.go
feedback Delete/Remark/Modify 不校验影响行数即返回 OKUpdates(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-51verify.go:34-35
order 多店铺下单 StoreID 记录错误 module/ec/order/internal/logic/summary/submit.go:91-93:166
market agency/fetchpending 的 identity 过滤未生效 module/ec/market/internal/logic/agency/fetch.go:35pending.go:36
market modify 返回的 Identity 恒为空 module/ec/market/internal/logic/agency/modify.go:53supply/modify.go:46
order coupon/by_statusIntroAmount module/ec/order/internal/logic/coupon/by_status.go:52
mall item_fetchidList 从未赋值;store/search 列名不符 module/ec/mall/internal/logic/product/item_fetch.go:28store/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.yamlService 写成 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 totalGroup + First,只返回一个分组行(空表返回 ErrDB module/base/logs/internal/logic/log/total.go:28
feedback list 过滤列写 username(列名是 user_nameagency 筛选恒假且可越权列表 module/base/feedback/internal/logic/method/list.go:38-50
feedback Modify 覆盖 identityFeedbackAccessory 未设 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,43comment_list.go:18-19
cms 点赞可刷量(op_identity 要求却从不使用) module/base/cms/internal/logic/post/incr_post_like.go:33
ads DB 未注入即 panicfts identity[0:2] 越界、Local/MinioOss/FtsConfig 不校验 module/base/ads/internal/logic/fetch/by_pos.go:21module/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 QuicklyCreateOrderGetSummaryCntGetSummaryList 无调用方
  • mall InitData()(默认店铺 + root 员工 seed无调用点
  • 各模块 cmd/cli/main.go 多为 Hello Worldpassportsenderinitial 的 README 描述的 Makefile/Dockerfile 在仓库中不存在
  • config.Spec.CodeCode.BlackListFilterCode.GenerateCodeCode.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.gotest/rpc/rpc.go 是 Hello World 或整段注释
  • fts internal/errorsinternal/response262 行)无导入方,internal/logic/fetch.go 整段注释,models/query.go:4-5 是空 InitData()
  • mgt builtinManagementMenuTemplates()ForgetPwdByPwdSaveTokenmodels/query.go 均无调用点
  • ads / cms / feedback / fts / logs / mgt 的 README 描述的 Makefile、Dockerfile、swagger、/health/metrics 在仓库中都不存在

11. 开发指引

11.1 本地运行

# 聚合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.Newimpl.NewImpl 在依赖缺失时会 panic,不是降级启动。

11.2 验证

go work sync
./scripts/verify-workspace.ps1     # Windowsgofmt + 每模块 go vet + go test
./scripts/verify-workspace.sh      # Linux/macOS
git diff --check

已知会使验证脚本失败的历史问题:module/base/fts/internal/routers/register_test.go 在工作区是 CRLF会被 gofmt 判为未格式化;同目录的 TestRoutesUseRESTModulePrefix 断言 /rest/fts/v1/* 而代码注册的是 /rest/fts/*(既有失败,与 ecmall 无关)。

11.3 增删一个服务

  1. pkgs/ecmall/internal/service/ 增加/删除对应的 xxx.go(照抄现有文件的 ExposeOptions 形态gRPC 型传 GRPC+GatewayREST 型传 Engine: srv.HTTP;注意该模块是否需要 Config
  2. 同步 internal/service/service.go:14-32services
  3. 同步 internal/config/:需要 Config 的模块要加结构体字段 + assignSharedConfig 分支(注意 fts / mgt 的 Config 不含 MicroService
  4. 同步 internal/config/config_test.goecmallServices 与必需配置节断言
  5. 同步 etc/default_dev.yamlServices 列表(以及新配置节、必要的匿名白名单)
  6. 同步 internal/service/service_test.go 的前缀/路由期望值
  7. go.mod 加/删 replace + require,然后 GOWORK=off go mod tidy
  8. go build ./... && go test ./...

internal/config/config_test.go 会强制「YAML 服务列表 == 注册表期望值」,internal/service/service_test.go 会强制「注册表 → 真实 gRPC/REST 面」,漏改会直接测试失败。

11.4 新增一个 RPC

标准路径:改 module/<域>/<模块>/proto/*.proto → 用 scripts/generate-protobuf.sh 重新生成 pb/ → 在 internal/server/<svc>_server.go 加方法(委托到 internal/logic/<子包>)→ 在 internal/logic/<子包>/ 加实现。网关路径会由生成的 *.pb.gw.go 自动出现为 POST /{package}.{Service}/{Method},动态 RPC 路径自动可用,无需改 pkgs/ecmall

11.5 新增配置段 / 匿名白名单

  • 专项配置:加在模块 internal/config 的结构体(要有 yaml tag聚合侧在 SrvConfig 加同名字段并在 assignSharedConfig 回填共享项,然后写进 pkgs/ecmall/etc/default_dev.yamlfts / 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 — 全部模块职责与实现边界
  • 系统架构 — 两个聚合入口的注册表差异、请求分流、依赖注入
  • 开发与运维 — 配置路径规则、验证/构建脚本、发布检查
  • API 接入总览 — gRPC / 动态 HTTP / gateway / 鉴权总览
  • 模块级 APIgRPC../../wiki/api/01-ads.md03-cms.md04-feedback.md05-initial.md06-passport.md07-sender.md08-address.md09-mall.md10-market.md11-order.md12-wallet.md
  • 模块级 API原生 REST../../wiki/api/16-fts-rest.md17-logs-rest.md18-mgt-rest.md