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

46 KiB
Raw Permalink Blame History

mall店铺与商品经营代码审计报告

内容
审计对象 module/ec/mall
服务域 电商-店铺与商品经营
审计日期 2026-09-22
代码规模 Go 文件 123 个(pb/ 生成代码 22 个,手写 101 个,手写约 5014 行;含生成代码共 21486 行);proto/ 8 个 1026 行;internal/logic/ 67 个文件(覆盖 7 个服务域)
入口 cmd/maingRPC + HTTP Gateway 单进程)、cmd/cli(占位)、聚合入口 pkgs/allservice.Expose 复用共享 gRPC/Gateway
对外协议 gRPC + HTTP Gateway路径 /mall.<Service>/<Method>,全部为 POST
结论摘要 接口数量大、分层一致,但越权面很宽13 处写操作与 3 处读操作只按 id/identity 定位、从不携带 token 中的 store_identityStaff.Fetch 鉴权被注释、手机验证码登录从不校验验证码、店铺支付/邮件配置可被任意已登录用户读改。另有 12 个明文占位接口(运费模板 5 个 + 禁运区域 5 个 + 入驻 + 小程序码 + 按规格查询)、item_fetch 分类过滤恒空、store/search 列名错误、spec_createstock_type 当库存写入等确定性缺陷。密码已统一为 bcrypt仅注释残留 MD5 字样。

1. 服务定位与职责

电商后台的经营域服务,围绕"店铺"组织全部资源:店铺基础配置与支付/邮件配置、店铺员工与其登录、商品(含规格、图片、评论、分类)、分类树、广告位、公告、运费模板与禁运区域。租户隔离通过 JWT owner 中的 store_id/store_identity 表达(bsm-sdk/core/types/encipher.go:8、签发见 internal/logic/staff/login.go:88)。

不负责:订单与购物车(module/ec/order)、地址库(module/ec/address)、供应商主数据(module/ec/market,本模块通过跨库表 market_supply 直读)。

2. 代码结构与入口

路径 职责
cmd/main/main.go 独立进程入口:config.Newimpl.NewImplserver.New(nil)service.Newsrv.Start()
cmd/cli/main.go 命令行占位入口,仅 log.Println("Hello World!")cmd/cli/main.go:6
internal/config/config.go 配置结构Base/Databases/MicroService/Rpc/Gateway/Apm/Etcd与端口、日志、密钥初始化
internal/impl/impl.go 初始化 Memory、Redis、DB、Etcd 四个全局实例
internal/password/password.go bcrypt 封装:Hash/Verify(唯一有测试的包)
internal/excode/ex.go 店铺域错误码 ErrStoreNotFound/ErrStoreDisabled
internal/models/** 16 张表的 GORM 模型 + 少量模型层 SQL 工具函数
internal/logic/{staff,store,product,category,freight,ads,notice}/** 业务逻辑67 个文件,按服务域分包
internal/server/** protoc-gen-slc 生成的 gRPC 服务端桩7 个 server + new.go 注册)
pb/** protoc 生成的 pb / grpc / gateway 代码22 个文件,本次只看接口签名)
service/expose.goservice/dependencies.go 聚合宿主注入:注册 7 组 gRPC + Gateway handler并允许外部覆盖 Redis/Etcd/DB/Cache
proto/*.proto 8 个契约文件const 为公共消息块)
etc/mall_{dev,test,prod}.yaml 三套配置,均为 postgres + Port: 12420/Gateway.Port: 12419
test/readme.md 仅一行 "restful test",无实际用例

依赖注入:service.Dependenciesservice/dependencies.go:12-17)在 pkgs/all 中由共享连接覆盖 impl 全局量;Expose 同时把 internal/server 的 7 个服务注册到宿主的 gRPC Server 与 Gateway Muxservice/expose.go:23-43)。

3. 接口清单

共 7 个服务、60 个 RPC全部映射为 POST /mall.<Service>/<Method>pb/*.pb.gw.goMustPattern,如 pb/staff.pb.gw.go:536-542、"审核" 列取自各 logic 文件实际调用的 service.ParseMetaCtx 参数)。

方法 路径 功能 鉴权 实现位置
Login /mall.Staff/Login 员工登录(账密 / 手机验证码) 无需 logic/staff/login.go:23
Fetch /mall.Staff/Fetch 员工列表 无(鉴权被注释) logic/staff/fetch.go:15(注释见 :17-20
Create /mall.Staff/Create 创建员工 Mall_Admin logic/staff/create.go:20
Delete /mall.Staff/Delete 删除员工 Mall_Admin logic/staff/delete.go:16
GetProfile /mall.Staff/GetProfile 员工资料 Mall_Admin logic/staff/get_profile.go:16
SetProfile /mall.Staff/SetProfile 改自己资料 Mall_Admin logic/staff/set_profile.go:16
SetPassword /mall.Staff/SetPassword 改自己密码 Mall_Admin logic/staff/set_password.go:18
ApplyJoin /mall.Store/ApplyJoin 申请入驻 Mall_Admin logic/store/apply_join.go:12 占位
Licensing /mall.Store/Licensing 店铺许可授权 logic/store/licensing.go:15 半占位
GetSetting /mall.Store/GetSetting 读店铺基础配置 logic/store/get_setting.go:14
SetSetting /mall.Store/SetSetting 写店铺基础配置 Mall_Admin不校验店铺归属 logic/store/set_setting.go:15
SetPayment /mall.Store/SetPayment 写支付配置 token任意角色 logic/store/set_payment.go:16
SetEmail /mall.Store/SetEmail 写邮件配置 token任意角色 logic/store/set_email.go:16
GetPayment /mall.Store/GetPayment 读支付配置 token任意角色 logic/store/get_payment.go:14
GetEmail /mall.Store/GetEmail 读邮件配置 token任意角色 logic/store/get_email.go:14
MiniCode /mall.Store/MiniCode 小程序推荐码 Mall_Admin logic/store/mini_code.go:11 占位(返回 nil
Search /mall.Store/Search 店铺搜索 Mall_Admin logic/store/search.go:16
ItemFetch /mall.Product/ItemFetch 商品列表 logic/product/item_fetch.go:15
ItemDetail /mall.Product/ItemDetail 商品详情 logic/product/item_detail.go:14
ItemDetailBySerial /mall.Product/ItemDetailBySerial 按序号批量取商品 logic/product/item_detail_by_serial.go:16
ItemDetailBySpec /mall.Product/ItemDetailBySpec 按规格编号取商品 logic/product/item_detail_by_spec.go:11 占位(返回 nil,nil
ItemCreate /mall.Product/ItemCreate 新建/更新商品 Mall_Admin logic/product/item_create.go:18
ItemDelete /mall.Product/ItemDelete 删除商品 Mall_Admin不校验店铺归属 logic/product/item_delete.go:16
ItemModify /mall.Product/ItemModify 上下架/禁用 Mall_Admin不校验店铺归属 logic/product/item_modify.go:16
ItemBatchOp /mall.Product/ItemBatchOp 批量改状态 Mall_Admin不校验店铺归属 logic/product/item_batch_op.go:16
SpecFetch /mall.Product/SpecFetch 规格列表 Mall_Admin不校验店铺归属 logic/product/spec_fetch.go:15
SpecCreate /mall.Product/SpecCreate 新建规格 Mall_Admin logic/product/spec_create.go:17
SpecModify /mall.Product/SpecModify 改规格 Mall_Admin不校验店铺归属 logic/product/spec_modify.go:15
SpecDelete /mall.Product/SpecDelete 删规格 Mall_Admin不校验店铺归属 logic/product/spec_delete.go:16
SpecDetail /mall.Product/SpecDetail 规格详情 logic/product/spec_detail.go:14
PhotoCreate /mall.Product/PhotoCreate 加图片 Mall_Admin logic/product/photo_create.go:17
PhotoDelete /mall.Product/PhotoDelete 删图片 Mall_Admin不校验店铺归属 logic/product/photo_delete.go:16
PhotoList /mall.Product/PhotoList 图片列表 logic/product/photo_list.go:14
CommentFetch /mall.Product/CommentFetch 评论列表 logic/product/comment_fetch.go:15
CommentCreate /mall.Product/CommentCreate 发评论/回复 is_comment=true 时任意 token否则 Mall_Admin logic/product/comment_create.go:18-24
CommentRe /mall.Product/CommentRe 商家回复 Mall_Admin logic/product/comment_re.go:17
CommentDelete /mall.Product/CommentDelete 删评论 Mall_Admin不校验店铺归属 logic/product/comment_delete.go:16
CommentModify /mall.Product/CommentModify 改评论 Mall_Admin不校验店铺归属 logic/product/comment_modify.go:15
Fetch /mall.Category/Fetch 分类树 logic/category/fetch.go:13
Create /mall.Category/Create 建分类 Mall_Admin logic/category/create.go:16
Delete /mall.Category/Delete 删分类 Mall_Admin不校验店铺归属 logic/category/delete.go:16
Modify /mall.Category/Modify 改分类 Mall_Admin不校验店铺归属 logic/category/modify.go:15
Fetch /mall.Freight/Fetch 运费模板列表 Mall_Admin logic/freight/fetch.go:11 占位
Create /mall.Freight/Create 建运费模板 Mall_Admin logic/freight/create.go:13 占位
Modify /mall.Freight/Modify 改运费模板 Mall_Admin logic/freight/modify.go:13 占位
Delete /mall.Freight/Delete 删运费模板 token任意角色 logic/freight/delete.go:13 占位
Detail /mall.Freight/Detail 运费模板详情 Mall_Admin logic/freight/detail.go:12 占位
DenyRegionFetch /mall.Freight/DenyRegionFetch 禁运区域列表 Mall_Admin logic/freight/deny_region_fetch.go:11 占位
DenyRegionCreate /mall.Freight/DenyRegionCreate 建禁运区域 Mall_Admin logic/freight/deny_region_create.go:13 占位
DenyRegionDelete /mall.Freight/DenyRegionDelete 删禁运区域 token任意角色 logic/freight/deny_region_delete.go:12 占位
DenyRegionModify /mall.Freight/DenyRegionModify 改禁运区域 Mall_Admin logic/freight/deny_region_modify.go:12 占位
ByPos /mall.Ads/ByPos 按广告位取广告 logic/ads/by_pos.go:13
Fetch /mall.Ads/Fetch 广告列表 logic/ads/fetch.go:14
Create /mall.Ads/Create 建广告 Mall_Admin logic/ads/create.go:18
Modify /mall.Ads/Modify 改广告 Mall_Admin不校验店铺归属 logic/ads/modify.go:17
Delete /mall.Ads/Delete 删广告 Mall_Admin不校验店铺归属 logic/ads/delete.go:16
Fetch /mall.Notice/Fetch 公告列表 logic/notice/fetch.go:14
Create /mall.Notice/Create 建公告 Mall_Admin logic/notice/create.go:18
Modify /mall.Notice/Modify 改公告 Mall_Admin不校验店铺归属 logic/notice/modify.go:17
Delete /mall.Notice/Delete 删公告 Mall_Admin不校验店铺归属 logic/notice/delete.go:16

契约以 proto 声明的类型为准(proto/*.proto),实现层的取参方式与 proto 一致;但 pb/ 生成代码只读了接口签名,未逐行审计。

声明但未实现 / 占位的接口(已核实)

接口 位置 现状
Store.ApplyJoin logic/store/apply_join.go:19-27 校验后仅剩两行 // TODO,直接返回 Code:0 "OK",不写 mall_apply、不建店铺与管理员
Store.Licensing logic/store/licensing.go:25,30 只按 subdomain 查店铺返回 identity请求中的 license_type/license 未参与校验(// TODO: valid code// TODO: add your logic code 仍保留
Store.MiniCode logic/store/mini_code.go:18-22 // TODO 后直接 returnMiniCodeReply 为 nil调用方拿到空消息
Product.ItemDetailBySpec logic/product/item_detail_by_spec.go:16-20 // TODOreturnListItem + nil error
Freight.{Fetch,Create,Modify,Delete,Detail} logic/freight/{fetch.go:26,create.go:24,modify.go:24,delete.go:25,detail.go:24} 5 个方法只做入参非空检查后返回固定 Code:0 "OK"Fetch/Detail 直接裸 return 空 replymall_freight/mall_freight_attr 表无任何读写
Freight.{DenyRegionFetch,DenyRegionCreate,DenyRegionDelete,DenyRegionModify} logic/freight/deny_region_{fetch.go:25,create.go:24,delete.go:24,modify.go:19-21}.go 同上5 个方法全为空实现,mall_freight_deny 表无任何读写
Freight.RemoveFreight.DenyRegionRemove logic/freight/remove.go:16logic/freight/deny_region_remove.go:13 反向问题:这两个方法写了真实删库逻辑,但 pb.FreightServer 接口只有 DenyRegionDelete/DenyRegionModifypb/freight_grpc.pb.goFreightServerinternal/server/freight_server.go 也只转调 freight.Delete/freight.DenyRegionDelete → 二者是不可达的死代码
models.InitData internal/models/query.go:36 默认店铺 + root 账号的初始化函数全仓无调用点,入口不会执行

其余残留 // TODO: add your logic code & delete this line. 但功能已实现的文件:logic/product/item_modify.go:28logic/product/spec_delete.go:33logic/product/photo_delete.go:34logic/notice/modify.go:27logic/notice/fetch.go:45logic/staff/fetch.go:49

4. 数据模型与表

16 张表全部经 GORM AutoMigrate 注册(模型 init()database.AppendMigrate)。

表名 模型文件 主键 / 唯一键 说明
mall_store internal/models/mall_store.go:15 id PKidentity 唯一索引(Std_IICUDS 店铺;pay_configs/mail_configs 为 text
mall_staff internal/models/mall_staff.go:19 id PKidentity 唯一索引;phone 唯一索引;account 普通索引 员工;password 存 bcrypt 串
mall_apply internal/models/mall_apply.go:16 id PKgorm.Model 入驻申请;无 identity 列,且全仓无任何读写代码
mall_product internal/models/mall_product.go:17 id PKidentity 唯一索引;serial_id 索引 商品主体
mall_product_spec internal/models/mall_product_spec.go:11 id PKidentity 唯一索引 规格;价格单位为分
product_spec internal/models/mall_product_link_spec.go:9 id PKidentity 唯一索引 商品-规格关联表(spec_id
product_category internal/models/mall_product_link_category.go:9 id PKidentity 唯一索引 商品-分类关联表
mall_product_photos internal/models/mall_product_photos.go:17 id PKidentity 唯一索引;product_id 索引 商品相册
mall_product_comment internal/models/mall_product_comment.go:17 id PKidentity 唯一索引 评论,自关联 Listcomment_identity
mall_product_attr internal/models/mall_product_attr.go:8 id PK 商品属性;全仓无读写代码
mall_category internal/models/mall_category.go:10 id PKidentity 唯一索引 分类树(自关联 Categories
mall_ads internal/models/mall_ads.go:8 id PKidentity 唯一索引 广告
mall_notice internal/models/mall_notice.go:8 id PKidentity 唯一索引 公告
mall_freight internal/models/mall_freight.go:10 id PKidentity 唯一索引 运费模板;无读写代码
mall_freight_attr internal/models/mall_freight_attr.go:9 id PKidentity 唯一索引 计费规则;无读写代码
mall_freight_deny internal/models/mall_freight_deny.go:10 id PKidentity 唯一索引 禁运区域;无读写代码

mall_storeinternal/models/mall_store.go:15

字段 类型 键/约束 说明
id uint PK 自增主键(Std_IICUDS
identity varchar(36) uniqueIndex 店铺唯一标识UUID
created_at/updated_at/deleted_at TIMESTAMP deleted_at 索引 软删除
status int8 default 0索引 1 正常 / -1 禁止
logo/title varchar(255) title not null 店铺标识与名称
intro/template/keywords varchar(255) default '' 简介、模板、SEO 关键词
subdomain varchar(255) not null 子域名,Licensing/Search 依赖
configs text default '' 基础配置JSON 字符串)
pay_configs text default '' 支付配置(含支付凭据)
mail_configs text default '' 邮件服务器配置(含 SMTP 凭据)
approve int8 default 0 0 待审 / 1 通过 / 2 失败

mall_staffinternal/models/mall_staff.go:19

字段 类型 键/约束 说明
id/identity/created_at/updated_at/deleted_at/status Std_IICUDS identity 唯一索引
store_id uint PK 成员(Std_Store 所属店铺 ID
store_identity varchar(36) 索引 所属店铺标识
name varchar(255) 索引default '' 姓名
account varchar(255) 普通索引(非唯一)default '' 登录账号;跨店可重名
password varchar(255) default '' bcrypt 哈希(internal/password/password.go
phone varchar(20) uniqueIndexdefault '' 手机号(验证码登录用)
email/avatar/profile varchar(255) default '' 邮箱、头像、简介
role varchar(36) 索引default '' Mall_Admin / staff
salt varchar(255) default '' 遗留盐字段,SetPassword 会显式清空(logic/staff/set_password.go:32
last_login_at TIMESTAMP 最后登录时间

mall_productinternal/models/mall_product.go:17

字段 类型 键/约束 说明
id/identity/时间/status Std_IICUDS identity 唯一索引 status1 上架 / 2 待上架 / 3 禁用 / 4 草稿 / 5 未上架
store_id/store_identity uint / varchar(36) 索引用 所属店铺(冗余存储)
serial_id varchar(36) Index 产品序号(gas_by_kg/gas_by_bottle 作特殊标识)
title varchar(255) not null 商品标题
sub_title/measuring_name/brand/standard_number/putaway_time/notes/args/category_paths varchar(255) default 副标题、计量单位、品牌、标准号、上架时间、备注、参数 JSON、分类路径
sell_max/sell_min/wastage/stock_type/stock int32 default 限购、损耗率、库存计数方式、库存
supply_id/template_id/li_template_id/recent_number/limitation/sale_total int64 default 供应商、运费模板、限区模板、近 30 天销量、区域限制、销售总数
sales_price/cost_price/partner_per/star int64 default 售价/成本价/分成/评分(sales_price 本模块从不写入)
recommend/types/gas_types/comment_total/view_total/image_total/sort int8/int32 default 推荐位、商品类型、燃气类型、计数、排序
content text default '' 商品详情
关联 foreignKey:ProductIdentity;references:Identity ImagesSpecsCategoriesmall_product.go:58-60

mall_product_specinternal/models/mall_product_spec.go:11

字段 类型 键/约束 说明
id uint PKgorm.Model 自增主键
identity varchar(36) 唯一索引 规格唯一标识UUID
product_identityproduct_spec 关联 product_spec.spec_id 商品归属靠关联表表达
title varchar(255) not null 规格名称
stock_type int32 default 2 1 设置库存限制 / 2 不限制
stock int64 default 0 库存数量(订单侧扣减目标,module/ec/order/internal/logic/summary/submit.go:209
price int64 default 0 销售价(分)
direct_sales int64 default 0 直营价(分)
purchasing_price int64 进货价(分),被匿名接口返回
wholesale int64 代理价(分),被匿名接口返回
tp_type/ds_tp_type int32 default 2 阶梯价开关
img/spec_no/keyword/serial_number varchar default '' 预览图、规格编号、关键词、产品编号
store_identity varchar(36) 店铺标识(由请求传入,见 6.1

无身份维度的跨库读取:logic/product/item_detail.go:31 直接查 market_supplymarket 模块的表);logic/product/item_fetch.go:51-63 直接 join product_category/mall_category/product_spec/mall_product_spec

5. 核心流程

flowchart TD
    A["POST /mall.Staff/Login"] --> B["LoginGenre"]
    B --> C{"1 账密"}
    B --> D{"2 手机验证码"}
    C --> E["按 account 查 mall_staff无店铺维度"]
    E --> F["password.Verifybcrypt"]
    D --> G["按 phone 查 mall_staff"]
    G --> H["只校验手机号格式,从不校验 verify_code"]
    F --> I["查店铺 status=1"]
    H --> I
    I --> J["encipher.GenerateTokenAes(Owner=Std_Store)"]
    J --> K["更新 last_login_at 并返回 token"]
flowchart TD
    A["POST /mall.Product/ItemCreate"] --> B["ParseMetaCtx 需 Mall_Admin"]
    B --> C["checkParamstatus=4 草稿可跳过)"]
    C --> D{"identity 为空"}
    D -->|是| E["itemToModel + 从 owner 取 store_id/store_identity"]
    D -->|否| F["itemToModel不回填 store 字段)"]
    E --> G["CreateProduct事务内写主表/规格/图片/分类"]
    F --> H["ModifyProduct先按 identity 查原记录,再事务内先删后建关联"]
    G --> I["返回 IdentityStatusReply"]
    H --> I
    J["POST /mall.Product/ItemFetch"] --> K["第一次分页查询(结果被丢弃)"]
    K --> L["按 category_id/keyword/价格区间/sort 组条件"]
    L --> M["第二次分页查询 + Preload Images/Specs.Spec/Categories"]
    M --> N["ModelsToProductItems 拼装返回"]

6. 审计发现

6.1 安全

级别 位置 问题
internal/logic/staff/login.go:45-58 手机验证码登录从不校验验证码login_genre=2 分支只检查 phone/verify_code 非空与手机号正则(:46),随后按手机号查库(:50)并签发 token:88),校验验证码的位置只留 // TODO: 添加验证码校验逻辑:58)。任意已注册手机号 + 任意非空 verify_code 即可取得该员工 JWT。
internal/logic/staff/fetch.go:17-20 鉴权代码被注释FetchParseMetaCtx 整段注释掉,/mall.Staff/Fetch 匿名可用。入参只有 store_identity:28),返回员工 account/phone/email/role/status:62-73)——只需知道店铺标识即可批量拉取员工手机号与邮箱。
internal/logic/store/{get_payment.go:16,26,set_payment.go:17,26,get_email.go:15,25,set_email.go:17,26} 四处都用 service.ParseMetaCtx(ctx, nil),只验 token 不验角色,且定位条件全部取自请求:读接口 Where("id=? or identity=?")、写接口 Where("identity = ?")set_payment.go:26set_email.go:26)。任意已登录账号(含 role=staff)可读/改任意店铺pay_configs/mail_configs
internal/logic/store/get_setting.go:14-23 GetSetting 完全没有鉴权调用,入参 id/identity 任一非空即返回该店铺基础配置(含 configs)。
internal/logic/store/set_setting.go:17,34 解析出的 auth 被丢弃(_),更新条件用请求里的 id/identity,任意 Mall_Admin 可改他人店铺的名称、子域名、模板、configs
internal/logic/product/{item_modify.go:29,item_batch_op.go:27,item_delete.go:28,spec_modify.go:34,spec_delete.go:28,photo_delete.go:28}internal/logic/category/{delete.go:28,modify.go:30}internal/logic/ads/delete.go:28internal/logic/notice/delete.go:28internal/logic/freight/remove.go:28internal/logic/staff/delete.go:28 13 处写操作一律只按 id/identity 定位对象,从不带上 token 中的 store_identity。同角色(Mall_Admin)可跨店铺修改/删除商品、规格、图片、评论、分类、广告、公告、运费模板、员工。反例参考:internal/models/mall_product.go:74 ForbiddenProduct 是唯一带 Where("store_identity = ?") 的批量更新实现,但没有任何调用点。
internal/logic/product/spec_detail.go:14-20internal/logic/product/item_detail.go:14-24 两个接口无鉴权,返回体经 internal/logic/product/ref.go:143-144PurchasingPrice/Wholesale)与 :54DirectSales)带出进货价与代理价;SpecDetail 还直接透传 PurchasingPricespec_detail.go:38)。
internal/logic/product/spec_fetch.go:17,22,27 仅要求 Mall_Admin 与入参 store_identity不校验该店铺是否属于调用方,一次请求即可批量拉走他店所有规格的价格、库存、进货价、代理价(无分页上限)。
internal/logic/store/licensing.go:15-38 Licensing 只用 domainsubdomain 后返回店铺 identity:41-49),请求中的许可码 in.License 从未参与校验(:25 // TODO: valid code)。知道子域名即可换取店铺标识。
internal/logic/product/spec_create.go:82 StoreIdentity: in.StoreIdentity 直接采信请求参数,规格所属店铺可被伪造;item_create.go:104-109 只在新建分支用 token 里的店铺字段,更新分支不回填。
internal/logic/product/item_detail.go:29-36 商品详情根据 supply_id 直查跨库表 market_supply 并回填 SupplyName,两个服务共享同一份库/表结构,无编译期约束。
internal/logic/staff/set_profile.go:31 业务查询上残留 impl.DBService.Debug(),会为该条 SQL 打开 GORM 调试日志(含参数)。
internal/logic/store/search.go:16-21 店铺搜索要求 Mall_Admin:18),但结果集是跨店铺的店铺列表(ModelToReply:50-73),角色语义不清。

6.2 正确性与逻辑缺陷

级别 位置 问题
internal/logic/product/item_fetch.go:28,50-54 idList 声明为空切片(:28)后从未把 in.CategoryId 填入,过滤条件变成 product_category.category_id in ?空集合GORM 生成 IN (NULL))→ 按分类筛选时结果恒为空
internal/logic/product/item_fetch.go:60-64 价格区间:条件是 `if min != 0
internal/logic/product/item_fetch.go:42-46:77-83 同一组分页条件被执行了两次(第一次 Count+Findproduct第二次整体覆盖第一次查询的结果完全作废DB 开销翻倍。
internal/logic/store/search.go:39 Where("keyword = ?", in.GetKeyword()),而 mall_store 的列名是 keywordsinternal/models/mall_store.go:25keywordmall_product 的列)→ 店铺搜索必然报 Unknown column,接口不可用。
internal/logic/product/spec_create.go:96-98 if in.StockType != 2 { specification.Stock = int64(in.StockType) }:把"库存设置开关"1 设置限制 / 2 不限制)当库存数量写入,客户端传 stock_type=1 时库存被覆盖为 1真正的 in.Stock 已在 :85 赋过值,随后被这一行改写。
internal/logic/product/spec_create.go:37-40internal/logic/product/photo_create.go:36-38 cnt, err := tx.RowsAffected, tx.Error; if cnt == 0 || err != nil { printer.Error(err.Error()) }cnt==0err==nil 时对 nil 调用 err.Error()panic,服务 500。
internal/logic/product/item_detail_by_serial.go:22 Preload("Spec")/Preload("Category") 与模型声明的关联名 Specs/Categoriesinternal/models/mall_product.go:59-60不一致。GORM 无法解析未声明的关联unsupported relation规格与分类不会加载且查询直接报错 → 该接口目前无法正常返回数据(对比同模块 item_detail.go:20 用的是正确名 Specs.Spec)。
internal/logic/product/comment_modify.go:22,31 校验要求 commentnick_name 非空(:22),但 Select("score","reply","product_identity","store_identity","spec_identity") 把这两列排除在更新范围外 → 评论正文和昵称永远改不了。
internal/logic/product/comment_re.go:24,31 要求 comment_identity 非空(:24),写入时却用 CommentIdentity: in.Identity:31)→ 被回复评论标识写成了回复自身。
internal/logic/product/comment_delete.go:28 Where("id=? or identity=? or comment_identity=?", in.GetId(), in.GetIdentity(), in.GetIdentity()),第三个条件把同一个 identity 当作 comment_identity,一次删除会连带删掉"以它为被回复对象"的所有回复。
internal/logic/ads/modify.go:35-38internal/logic/notice/modify.go:32-34 两处都在结构体里设置 Status,但 Select 白名单不含 statusads 为 title,pos_key,content,type,to_urlnotice 为 title,author,content)→ 广告/公告的启用与禁用实际不生效。
internal/logic/store/get_setting.go:26-38 返回体未设置 Statusproto StoreBasic.status 恒为 0proto/store.proto:86),前端无法区分店铺是否停用。
internal/logic/staff/login.go:71,79-81 查询已带 status = 1,随后的 if storeData.Status == 0 永不成立(死判断);login.go:32account 查而不带店铺维度,mall_staff.account 只是普通索引(internal/models/mall_staff.go:23)而非唯一键 → 跨店同名账号时取到任意一条记录,可能落到错误店铺。
internal/logic/product/item_create.go:38-44internal/models/mall_product.go:153 更新走 ModifyProduct,其 tx.Where("identity = ?", product.Identity).Updates(product) 传的是结构体GORM 跳过零值字段 → 已填字段无法清空(如把 sell_max 改回 0、把 notes 清空)。
internal/logic/category/fetch.go:19-24internal/models/query.go:75 typesall/first)只影响是否递归子集,GetCategoryList 恒以 parent_id = 0 过滤顶层,两个取值语义无差别;types=first 的层级语义未落地。
internal/logic/ads/by_pos.go:20 Find(&result) 的目标是 []*pb.AdsItemprotobuf 结构体),直接让 GORM 把 mall_ads 行扫进 pb 类型,字段映射依赖命名约定而非显式列;同模块其它查询都先落 models.MallAdsref() 转换(ads/fetch.go:39-49)。
internal/logic/product/item_modify.go:24 if in.GetIdentity() == "" || in.GetStatus() == 0 拒绝 status=0,但 proto 未定义 0实际等价于"必须传 status";上传未上架时无法回退到 0。
多处 时间戳单位混用:item_create.go:55UnixMilli()ads/create.go:51category/modify.go:40freight/create.go:29freight/deny_region_*Unix()(秒),item_delete.go:37photo_delete.go:39UnixNano()
internal/logic/store/get_payment.go:26internal/logic/store/get_email.go:25 Find 而非 First:查不到店铺时返回空 Configs 而不报错,调用方无法区分"无配置"与"店铺不存在"。

6.3 未完成实现

完整占位清单见第 3 节末尾表格。归纳:

  • 运费与禁运区域整体未实现Freight 服务 9 个 RPC 中 9 个为空实现(Create/Modify/Delete/Detail/Fetch/DenyRegion*mall_freightmall_freight_attrmall_freight_deny 三张表建好了但无任何读写代码。前端调用会拿到 Code:0 "OK" 的假成功。
  • 入驻与授权Store.ApplyJoin 返回假成功(logic/store/apply_join.go:19-27mall_apply 表全仓无读写;Store.Licensing 的许可码校验缺失(logic/store/licensing.go:25)。
  • 小程序码Store.MiniCode 为空(logic/store/mini_code.go:18-22),返回 nil reply。
  • 按规格查询Product.ItemDetailBySpec 为空(logic/product/item_detail_by_spec.go:16-20)。
  • 批量操作Product.ItemBatchOp 已实现,但只用 Where("identity in ?")logic/product/item_batch_op.go:27),已有的 ForbiddenProduct(带店铺约束,internal/models/mall_product.go:74)未被接线。
  • 反向未接线freight/remove.go:16freight/deny_region_remove.go:13 实现完整但不在 proto 服务接口中(pb/freight_grpc.pb.goFreightServer 只有 DenyRegionFetch/Create/Delete/Modifyinternal/server/freight_server.go 未转调 → 死代码。
  • 初始化未接线internal/models/query.go:36 InitData(建默认店铺 + root/123456 管理员)无调用点。

6.4 健壮性与可维护性

级别 位置 问题
internal/config/config.go:38 conf.NotNil(Spec.Service, Spec.Cache) 未校验 Databases,而 internal/impl/impl.go:26 直接用 config.Spec.Databases;漏配时 DB 为 nil首个查询即空指针。
internal/server/new.go:26-30 + cmd/main/main.go:35 Server 结构体有 Mux 字段(new.go:16)但 New() 从未初始化,main.go:35 把它作为 GatewayMux 传给 SDKServer.Mux 为 nil 时 SDK 的 http.ListenAndServe(addr, mux)bsm-sdk/core/service/service.go:126)实际挂的是 http.DefaultServeMuxservice/expose.go:23-43 注册的 8 组 HTTP 路由在独立进程里不会生效(全部 404
internal/logic/store/search.go:34internal/logic/ads/fetch.go:34internal/logic/notice/fetch.go:35internal/logic/product/comment_fetch.go:38internal/logic/staff/fetch.go:37 分页一律写成 if pageSize < 50 { pageSize = 50 }只有下限没有上限item_fetch.go:37 只在 <= 0 时兜底 10。客户端传 page_size=1000000 即触发全表扫描与超大响应。
internal/logic/store/{get_setting.go:21,set_setting.go:35,get_payment.go:27,get_email.go:26} 错误上报用 Go 内置 print(err.Error())(写 stderr、无级别、无结构与同模块其它位置的 printer.Error 混用。
internal/models/mall_product.go:74,80,206,249internal/models/mall_product_spec.go:41,48,58internal/models/mall_product_photos.go:38,48internal/models/mall_product_comment.go:46,52,58,64 13 个模型层函数全仓无调用者(ForbiddenProductProductDetailDelProductGetProductListDetailSpecSpecListDelSpecPhotoListDelPhotoDelCommentCreateCommentReCommentModifyComment)。其中 DelProduct(事务级联删评论/图片/规格/分类)比实际使用的单表删除(item_delete.go:28)更完整,却没有接线。
internal/logic/product/item_detail.go:35 reply.SupplyName = supply["name"].(string) 无保护类型断言,且 supply 来自跨库表 market_supply;该列为 NULL 或类型变化即 panic。
internal/impl/impl.go:22 + service/dependencies.go:30 MemorySerice 拼写错误(应为 MemoryService),且除依赖注入赋值外全模块无使用点。
全模块 仅 1 个测试文件 internal/password/password_test.gobcrypt 正反例);test/readme.md 仅一行 "restful test"67 个 logic 文件 0 测试。
cmd/cli/main.go:6 CLI 入口只打印 "Hello World!"。
README.md:39,219-229,375-389etc/mall_prod.yaml:2,5-7,24 README 要求 MySQL 8.0+、端口写 12101/12102,实际配置是 Driver: postgresPort: 12420Gateway.Port: 12419README 的"缓存策略表/连接池/读写分离"在代码中不存在(impl.go:24-28 只建了连接,无缓存读写)。
etc/mall_dev.yaml:19-20prod/test 同) Anonymous: - mall.ping.hello,但 8 个 proto 文件与全部 gw 路由中都不存在 ping 方法,是死配置。
internal/logic/staff/login.go:40 注释写"使用MD5加盐验证",实际调用 password.Verifybcrypt:41)。全仓已无 MD5 实现md5 仅此一处注释命中);但历史以 MD5 入库的账号 bcrypt.CompareHashAndPassword 必然失败,等于无法登录,且 mall_staff.salt 字段(mall_staff.go:30)无迁移处理。

7. 风险汇总

编号 级别 问题 影响面
M1 手机验证码登录不校验验证码 任意账号接管
M2 支付/邮件配置可被任意已登录用户读改4 个接口 + 无鉴权的 GetSetting 店铺资金与邮件凭据泄露、被篡改
M3 12 处写操作无店铺归属校验(商品/规格/图片/分类/广告/公告/运费/员工),评论的改删另见 M13 跨店数据被改删
M4 Staff.Fetch 鉴权被注释 员工手机号/邮箱批量泄露
M5 进货价/代理价经无鉴权的 ItemDetailSpecDetail 与无归属校验的 SpecFetch 外泄 商业成本信息泄露
M6 Licensing 不校验许可码 店铺标识可被枚举获取
M7 item_fetch 分类过滤恒空、价格区间单边失效、重复查询 商品列表功能不可用 + DB 压力翻倍
M8 store/search.go:39 列名错误 店铺搜索完全不可用
M9 spec_create.go:96-98 库存被写为 stock_type 库存数据错误、影响下单扣减
M10 spec_create.go:38photo_create.go:37 nil error 调 Error() 服务 panic
M11 item_detail_by_serial.go:22 Preload 关联名不存在 接口不可用
M12 12 个接口空实现/假成功(运费 9 个 + 入驻 + 小程序码 + 按规格查询) 业务功能缺失且无错误反馈
M13 评论修改/回复/删除的目标字段写错或被 Select 排除 评论功能行为异常、误删
M14 广告/公告状态更新被 Select 白名单排除 启停失效
M15 登录跨店同名账号、店铺状态死判断 登录归属不确定
M16 GORM 分页无上限、print 上报、死代码、无测试 可维护性与抗压能力
M17 Databases 未校验、Server.Mux 未初始化 启动后运行期崩溃 / 独立进程 HTTP 全 404
M18 README 与配置不符、Anonymous 死配置、MD5 注释残留 认知成本与迁移风险

8. 修复建议(务实项)

  1. 补验证码校验M1login.go:58 的 TODO 必须落地——按 phone 取出服务端存的一次性验证码,比对 in.VerifyCode 及其有效期,成功后立即删除;在此之前建议直接对该分支返回 ErrUnimplemented,不要签发 token。
  2. 恢复 Staff.Fetch 鉴权并收敛返回字段M4取消 fetch.go:17-20 的注释,改为 &service.ParseOptions{RoleValue: "Mall_Admin"},并把 store_identity 从入参改为取 token 中的 ownerModelToReply 去掉 phone/email(或按需脱敏)。
  3. 给所有写操作补店铺归属约束M3统一在 Where 后追加 store_identity = ?(值取自 auth.Owner)。已有现成范例:internal/models/mall_product.go:75ForbiddenProduct,把 item_batch_op.go:27item_modify.go:29item_delete.go:28spec_*photo_delete.go:28、分类/广告/公告/员工的对应 Delete/Modify/Updates 都对齐这个写法;SpecCreateStoreIdentityspec_create.go:82)改为从 token 取。
  4. 收口配置类接口M2get_payment/set_payment/get_email/set_emailParseMetaCtx(ctx, nil) 改成 RoleValue: "Mall_Admin",并把 identity 换成 token 中的 store_identity(或校验两者相等后拒绝不匹配);GetSetting 补上与 SetSetting 相同的鉴权;返回 pay_configs/mail_configs 时对凭据字段做掩码。
  5. item_fetch 三处修复M7item_fetch.go:28 改为 idList = in.GetCategoryId() 并删掉 :42-46 的第一次查询;价格过滤拆成"只传 min / 只传 max / 都传"三分支join 规格后加 Group("mall_product.id") 或改用 DISTINCT
  6. store/search.go:39 改列名Where("keywords like ?", "%"+in.GetKeyword()+"%")(或按业务决定不加过滤),并补 page_size 上限。
  7. spec_create.go 修正:删除 :96-98 这行,库存只认 in.Stock;把 :38-40photo_create.go:37-39err.Error() 改成先判 err != nil 再打印(或直接 return errcode.ErrDB)。
  8. item_detail_by_serial.go:22 改关联名Preload("Specs.Spec")Preload("Categories.Category"),与 item_detail.go:20 保持一致。
  9. comment_modify.go:31:把 commentnickname 加进 Select 白名单;comment_re.go:31 改为 CommentIdentity: in.GetCommentIdentity()comment_delete.go:28 去掉第三个 comment_identity=? 条件或改用独立入参。
  10. ads/modify.go:38notice/modify.go:34:把 status 加进 Select,或改用 map[string]any 显式指定要更新的列。
  11. 占位接口二选一Freight 的 9 个方法与 ApplyJoin/MiniCode/ItemDetailBySpec 要么按 proto 实现,要么暂时返回 errcode.ErrUnimplemented,不要返回 Code:0 "OK";同时把 freight/remove.gofreight/deny_region_remove.go 接到 pb.FreightServer 上(或删除,避免两份实现并存)。
  12. 配置与启动config.go:38conf.NotNil 补上 Databasesinternal/server/new.go 初始化 Mux: gwRuntime.NewServeMux(),否则独立进程的 HTTP 入口无任何路由。
  13. 分页上限:在 search.go:34ads/fetch.go:34notice/fetch.go:35comment_fetch.go:38staff/fetch.go:37item_fetch.go:37 统一补 if pageSize > 200 { pageSize = 200 }
  14. 清理与测试:删除 set_profile.go:31.Debug()get_setting.go 等 4 处 print(err.Error()) 与 6 处 TODO 残留;为 login(含验证码分支)、item_fetchspec_createitem_create 各补 1 条正例 + 1 条边界用例README 的驱动与端口按 etc/*.yaml 更新。

本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。仓储里"为 models 层加统一查询构造器""引入 DTO/VO 转换层""把跨库表访问抽象为 RPC 客户端"一类改造不在此列。

9. 整改记录2026-09-22

本节记录按本报告结论执行的代码整改。整改遵循最小修正原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界未修改任何 proto/*.proto 与生成的 pb/*.go;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:GOWORK=off go build ./... + GOWORK=off go vet ./... + gofmt -l(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题

编号 级别 问题 处理结果
M1 员工手机验证码登录不校验验证码 已修复:按 /SMS/Code/ + 手机号的既有 Redis 键规则真实校验,成功后删除键
M2 支付/邮件配置可被任意已登录用户读改4 接口 + 无鉴权的 GetSetting 已修复:统一增加「当前登录者属于该店铺」的归属校验,越权返回 ErrPermissionDeniedSetSetting 亦从「凭请求 identity 直接 Updates」改为以 token 中的店铺归属为准,并检查 RowsAffected,避免改到他人店铺却静默成功
M3 12 处写操作无店铺归属校验(商品/规格/图片/分类/广告/公告/运费/员工) 已修复:逐处补归属条件(条件中直接带归属字段,RowsAffected == 0 即视为越权或不存在)
M4 Staff.Fetch 鉴权被注释,员工手机号/邮箱批量泄露 已修复:恢复鉴权
M5 进货价/代理价经 ItemDetailSpecDetailSpecFetch 外泄 已修复:匿名或非本店调用者不下发成本类价格字段,本店可正常查看
M6 Licensing 不校验许可码,店铺标识可被枚举 已修复(安全降级):要求已登录并校验归属。完整许可校验缺少数据源,未做
M7 item_fetch 分类过滤恒空、价格区间单边失效、重复查询 已修复:修正过滤条件与重复查询
M8 store/search.go 列名错误导致店铺搜索完全不可用 已修复:按模型真实列名修正
M9 spec_create.go 库存被写为 stock_type 已修复:库存写入正确字段
M10 spec_create.go/photo_create.go nil error 调 Error() 导致 panic 已修复:先判 err 非空再记录
M11 item_detail_by_serial.go Preload 关联名不存在 已修复:按模型真实关联名修正
M12 12 个接口空实现/假成功(运费 9 个 + 入驻 + 小程序码 + 按规格查询) 已修复:全部改为显式返回 errcode.ErrUnimplemented,不再假成功;方法签名与 proto 保持不变。其中 freight.DenyRegionRemove 在首轮整改中被遗漏,本轮已补齐(共 9 个运费接口)

未纳入本轮范围

报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)本轮未处理;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。

本轮整改未修改任何 proto/*.protopb/*.go,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。