# mall(店铺与商品经营)代码审计报告 | 项 | 内容 | | --- | --- | | 审计对象 | `module/ec/mall` | | 服务域 | 电商-店铺与商品经营 | | 审计日期 | 2026-09-22 | | 代码规模 | Go 文件 123 个(`pb/` 生成代码 22 个,手写 101 个,手写约 5014 行;含生成代码共 21486 行);`proto/` 8 个 1026 行;`internal/logic/` 67 个文件(覆盖 7 个服务域) | | 入口 | `cmd/main`(gRPC + HTTP Gateway 单进程)、`cmd/cli`(占位)、聚合入口 `pkgs/all` 经 `service.Expose` 复用共享 gRPC/Gateway | | 对外协议 | gRPC + HTTP Gateway,路径 `/mall./`,全部为 POST | | 结论摘要 | 接口数量大、分层一致,但**越权面很宽**:13 处写操作与 3 处读操作只按 `id/identity` 定位、从不携带 token 中的 `store_identity`;`Staff.Fetch` 鉴权被注释、手机验证码登录从不校验验证码、店铺支付/邮件配置可被任意已登录用户读改。另有 12 个明文占位接口(运费模板 5 个 + 禁运区域 5 个 + 入驻 + 小程序码 + 按规格查询)、`item_fetch` 分类过滤恒空、`store/search` 列名错误、`spec_create` 把 `stock_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.New` → `impl.NewImpl` → `server.New(nil)` → `service.New` → `srv.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.go`、`service/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.Dependencies`(`service/dependencies.go:12-17`)在 `pkgs/all` 中由共享连接覆盖 `impl` 全局量;`Expose` 同时把 `internal/server` 的 7 个服务注册到宿主的 gRPC Server 与 Gateway Mux(`service/expose.go:23-43`)。 ## 3. 接口清单 共 7 个服务、60 个 RPC,全部映射为 `POST /mall./`(`pb/*.pb.gw.go` 的 `MustPattern`,如 `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` 后直接 `return`,`MiniCodeReply` 为 nil,调用方拿到空消息 | | `Product.ItemDetailBySpec` | `logic/product/item_detail_by_spec.go:16-20` | `// TODO` 后 `return` 空 `ListItem` + 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` 空 reply),`mall_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.Remove`、`Freight.DenyRegionRemove` | `logic/freight/remove.go:16`、`logic/freight/deny_region_remove.go:13` | **反向问题**:这两个方法写了真实删库逻辑,但 `pb.FreightServer` 接口只有 `DenyRegionDelete`/`DenyRegionModify`(`pb/freight_grpc.pb.go` 的 `FreightServer`),`internal/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:28`、`logic/product/spec_delete.go:33`、`logic/product/photo_delete.go:34`、`logic/notice/modify.go:27`、`logic/notice/fetch.go:45`、`logic/staff/fetch.go:49`。 ## 4. 数据模型与表 16 张表全部经 GORM `AutoMigrate` 注册(模型 `init()` 调 `database.AppendMigrate`)。 | 表名 | 模型文件 | 主键 / 唯一键 | 说明 | | --- | --- | --- | --- | | `mall_store` | `internal/models/mall_store.go:15` | `id` PK;`identity` 唯一索引(`Std_IICUDS`) | 店铺;`pay_configs`/`mail_configs` 为 text | | `mall_staff` | `internal/models/mall_staff.go:19` | `id` PK;`identity` 唯一索引;`phone` 唯一索引;`account` 普通索引 | 员工;`password` 存 bcrypt 串 | | `mall_apply` | `internal/models/mall_apply.go:16` | `id` PK(`gorm.Model`) | 入驻申请;**无 identity 列,且全仓无任何读写代码** | | `mall_product` | `internal/models/mall_product.go:17` | `id` PK;`identity` 唯一索引;`serial_id` 索引 | 商品主体 | | `mall_product_spec` | `internal/models/mall_product_spec.go:11` | `id` PK;`identity` 唯一索引 | 规格;价格单位为分 | | `product_spec` | `internal/models/mall_product_link_spec.go:9` | `id` PK;`identity` 唯一索引 | 商品-规格关联表(`spec_id`) | | `product_category` | `internal/models/mall_product_link_category.go:9` | `id` PK;`identity` 唯一索引 | 商品-分类关联表 | | `mall_product_photos` | `internal/models/mall_product_photos.go:17` | `id` PK;`identity` 唯一索引;`product_id` 索引 | 商品相册 | | `mall_product_comment` | `internal/models/mall_product_comment.go:17` | `id` PK;`identity` 唯一索引 | 评论,自关联 `List`(`comment_identity`) | | `mall_product_attr` | `internal/models/mall_product_attr.go:8` | `id` PK | 商品属性;**全仓无读写代码** | | `mall_category` | `internal/models/mall_category.go:10` | `id` PK;`identity` 唯一索引 | 分类树(自关联 `Categories`) | | `mall_ads` | `internal/models/mall_ads.go:8` | `id` PK;`identity` 唯一索引 | 广告 | | `mall_notice` | `internal/models/mall_notice.go:8` | `id` PK;`identity` 唯一索引 | 公告 | | `mall_freight` | `internal/models/mall_freight.go:10` | `id` PK;`identity` 唯一索引 | 运费模板;**无读写代码** | | `mall_freight_attr` | `internal/models/mall_freight_attr.go:9` | `id` PK;`identity` 唯一索引 | 计费规则;**无读写代码** | | `mall_freight_deny` | `internal/models/mall_freight_deny.go:10` | `id` PK;`identity` 唯一索引 | 禁运区域;**无读写代码** | ### 表 `mall_store`(`internal/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_staff`(`internal/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) | uniqueIndex,default `''` | 手机号(验证码登录用) | | `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_product`(`internal/models/mall_product.go:17`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id`/`identity`/时间/`status` | 同 `Std_IICUDS` | `identity` 唯一索引 | `status`:1 上架 / 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` | `Images`、`Specs`、`Categories`(`mall_product.go:58-60`) | ### 表 `mall_product_spec`(`internal/models/mall_product_spec.go:11`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` | uint | PK(`gorm.Model`) | 自增主键 | | `identity` | varchar(36) | 唯一索引 | 规格唯一标识(UUID) | | `product_identity` 经 `product_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_supply`(market 模块的表);`logic/product/item_fetch.go:51-63` 直接 join `product_category`/`mall_category`/`product_spec`/`mall_product_spec`。 ## 5. 核心流程 ```mermaid flowchart TD A["POST /mall.Staff/Login"] --> B["LoginGenre"] B --> C{"1 账密"} B --> D{"2 手机验证码"} C --> E["按 account 查 mall_staff(无店铺维度)"] E --> F["password.Verify(bcrypt)"] 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"] ``` ```mermaid flowchart TD A["POST /mall.Product/ItemCreate"] --> B["ParseMetaCtx 需 Mall_Admin"] B --> C["checkParam(status=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` | **鉴权代码被注释**:`Fetch` 的 `ParseMetaCtx` 整段注释掉,`/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:26`、`set_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:28`、`internal/logic/notice/delete.go:28`、`internal/logic/freight/remove.go:28`、`internal/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-20`、`internal/logic/product/item_detail.go:14-24` | 两个接口**无鉴权**,返回体经 `internal/logic/product/ref.go:143-144`(`PurchasingPrice`/`Wholesale`)与 `:54`(`DirectSales`)带出进货价与代理价;`SpecDetail` 还直接透传 `PurchasingPrice`(`spec_detail.go:38`)。 | | **高** | `internal/logic/product/spec_fetch.go:17,22,27` | 仅要求 `Mall_Admin` 与入参 `store_identity`,**不校验该店铺是否属于调用方**,一次请求即可批量拉走他店所有规格的价格、库存、进货价、代理价(无分页上限)。 | | **高** | `internal/logic/store/licensing.go:15-38` | `Licensing` 只用 `domain` 查 `subdomain` 后返回店铺 `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 || max != 0`,取值却固定 `between min and max`;只传 `min_price` 时 `max=0`,区间为 `[min,0]` 恒空。同时 join 规格表未去重,同一商品命中多条规格会重复出现在结果中。 | | **高** | `internal/logic/product/item_fetch.go:42-46` 与 `:77-83` | 同一组分页条件被**执行了两次**(第一次 `Count`+`Find` 到 `product`,第二次整体覆盖),第一次查询的结果完全作废,DB 开销翻倍。 | | **高** | `internal/logic/store/search.go:39` | `Where("keyword = ?", in.GetKeyword())`,而 `mall_store` 的列名是 `keywords`(`internal/models/mall_store.go:25`,`keyword` 是 `mall_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-40`、`internal/logic/product/photo_create.go:36-38` | `cnt, err := tx.RowsAffected, tx.Error; if cnt == 0 \|\| err != nil { printer.Error(err.Error()) }`:`cnt==0` 而 `err==nil` 时对 nil 调用 `err.Error()` → **panic**,服务 500。 | | **高** | `internal/logic/product/item_detail_by_serial.go:22` | `Preload("Spec")`/`Preload("Category")` 与模型声明的关联名 `Specs`/`Categories`(`internal/models/mall_product.go:59-60`)不一致。GORM 无法解析未声明的关联(unsupported relation),规格与分类不会加载且查询直接报错 → 该接口目前无法正常返回数据(对比同模块 `item_detail.go:20` 用的是正确名 `Specs.Spec`)。 | | 中 | `internal/logic/product/comment_modify.go:22,31` | 校验要求 `comment` 与 `nick_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-38`、`internal/logic/notice/modify.go:32-34` | 两处都在结构体里设置 `Status`,但 `Select` 白名单不含 `status`(ads 为 `title,pos_key,content,type,to_url`,notice 为 `title,author,content`)→ 广告/公告的启用与禁用实际不生效。 | | 中 | `internal/logic/store/get_setting.go:26-38` | 返回体未设置 `Status`(proto `StoreBasic.status` 恒为 0,`proto/store.proto:86`),前端无法区分店铺是否停用。 | | 中 | `internal/logic/staff/login.go:71,79-81` | 查询已带 `status = 1`,随后的 `if storeData.Status == 0` 永不成立(死判断);`login.go:32` 按 `account` 查而不带店铺维度,`mall_staff.account` 只是普通索引(`internal/models/mall_staff.go:23`)而非唯一键 → 跨店同名账号时取到任意一条记录,可能落到错误店铺。 | | 中 | `internal/logic/product/item_create.go:38-44`、`internal/models/mall_product.go:153` | 更新走 `ModifyProduct`,其 `tx.Where("identity = ?", product.Identity).Updates(product)` 传的是结构体,GORM 跳过零值字段 → 已填字段无法清空(如把 `sell_max` 改回 0、把 `notes` 清空)。 | | 中 | `internal/logic/category/fetch.go:19-24`、`internal/models/query.go:75` | `types`(`all`/`first`)只影响是否递归子集,`GetCategoryList` 恒以 `parent_id = 0` 过滤顶层,两个取值语义无差别;`types=first` 的层级语义未落地。 | | 中 | `internal/logic/ads/by_pos.go:20` | `Find(&result)` 的目标是 `[]*pb.AdsItem`(protobuf 结构体),直接让 GORM 把 `mall_ads` 行扫进 pb 类型,字段映射依赖命名约定而非显式列;同模块其它查询都先落 `models.MallAds` 再 `ref()` 转换(`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:55` 用 `UnixMilli()`,`ads/create.go:51`、`category/modify.go:40`、`freight/create.go:29`、`freight/deny_region_*` 用 `Unix()`(秒),`item_delete.go:37`、`photo_delete.go:39` 用 `UnixNano()`。 | | 低 | `internal/logic/store/get_payment.go:26`、`internal/logic/store/get_email.go:25` | 用 `Find` 而非 `First`:查不到店铺时返回空 `Configs` 而不报错,调用方无法区分"无配置"与"店铺不存在"。 | ### 6.3 未完成实现 完整占位清单见第 3 节末尾表格。归纳: - **运费与禁运区域整体未实现**:`Freight` 服务 9 个 RPC 中 9 个为空实现(`Create`/`Modify`/`Delete`/`Detail`/`Fetch`/`DenyRegion*`),`mall_freight`、`mall_freight_attr`、`mall_freight_deny` 三张表建好了但无任何读写代码。前端调用会拿到 `Code:0 "OK"` 的假成功。 - **入驻与授权**:`Store.ApplyJoin` 返回假成功(`logic/store/apply_join.go:19-27`),`mall_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:16`、`freight/deny_region_remove.go:13` 实现完整但不在 proto 服务接口中(`pb/freight_grpc.pb.go` 的 `FreightServer` 只有 `DenyRegionFetch/Create/Delete/Modify`),`internal/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` 传给 SDK;`Server.Mux` 为 nil 时 SDK 的 `http.ListenAndServe(addr, mux)`(`bsm-sdk/core/service/service.go:126`)实际挂的是 `http.DefaultServeMux`,`service/expose.go:23-43` 注册的 8 组 HTTP 路由在独立进程里不会生效(全部 404)。 | | 中 | `internal/logic/store/search.go:34`、`internal/logic/ads/fetch.go:34`、`internal/logic/notice/fetch.go:35`、`internal/logic/product/comment_fetch.go:38`、`internal/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,249`、`internal/models/mall_product_spec.go:41,48,58`、`internal/models/mall_product_photos.go:38,48`、`internal/models/mall_product_comment.go:46,52,58,64` | 13 个模型层函数全仓无调用者(`ForbiddenProduct`、`ProductDetail`、`DelProduct`、`GetProductList`、`DetailSpec`、`SpecList`、`DelSpec`、`PhotoList`、`DelPhoto`、`DelComment`、`CreateComment`、`ReComment`、`ModifyComment`)。其中 `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.go`(bcrypt 正反例);`test/readme.md` 仅一行 "restful test";67 个 logic 文件 0 测试。 | | 低 | `cmd/cli/main.go:6` | CLI 入口只打印 "Hello World!"。 | | 低 | `README.md:39,219-229,375-389` 与 `etc/mall_prod.yaml:2,5-7,24` | README 要求 MySQL 8.0+、端口写 `12101`/`12102`,实际配置是 `Driver: postgres`、`Port: 12420`、`Gateway.Port: 12419`;README 的"缓存策略表/连接池/读写分离"在代码中不存在(`impl.go:24-28` 只建了连接,无缓存读写)。 | | 低 | `etc/mall_dev.yaml:19-20`(prod/test 同) | `Anonymous: - mall.ping.hello`,但 8 个 proto 文件与全部 gw 路由中都不存在 `ping` 方法,是死配置。 | | 低 | `internal/logic/staff/login.go:40` | 注释写"使用MD5加盐验证",实际调用 `password.Verify`(bcrypt,`: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 | 高 | 进货价/代理价经无鉴权的 `ItemDetail`、`SpecDetail` 与无归属校验的 `SpecFetch` 外泄 | 商业成本信息泄露 | | M6 | 高 | `Licensing` 不校验许可码 | 店铺标识可被枚举获取 | | M7 | 高 | `item_fetch` 分类过滤恒空、价格区间单边失效、重复查询 | 商品列表功能不可用 + DB 压力翻倍 | | M8 | 高 | `store/search.go:39` 列名错误 | 店铺搜索完全不可用 | | M9 | 高 | `spec_create.go:96-98` 库存被写为 `stock_type` | 库存数据错误、影响下单扣减 | | M10 | 高 | `spec_create.go:38`、`photo_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. **补验证码校验**(M1):`login.go:58` 的 TODO 必须落地——按 `phone` 取出服务端存的一次性验证码,比对 `in.VerifyCode` 及其有效期,成功后立即删除;在此之前建议直接对该分支返回 `ErrUnimplemented`,不要签发 token。 2. **恢复 `Staff.Fetch` 鉴权并收敛返回字段**(M4):取消 `fetch.go:17-20` 的注释,改为 `&service.ParseOptions{RoleValue: "Mall_Admin"}`,并把 `store_identity` 从入参改为取 token 中的 `owner`,`ModelToReply` 去掉 `phone`/`email`(或按需脱敏)。 3. **给所有写操作补店铺归属约束**(M3):统一在 `Where` 后追加 `store_identity = ?`(值取自 `auth.Owner`)。已有现成范例:`internal/models/mall_product.go:75` 的 `ForbiddenProduct`,把 `item_batch_op.go:27`、`item_modify.go:29`、`item_delete.go:28`、`spec_*`、`photo_delete.go:28`、分类/广告/公告/员工的对应 `Delete`/`Modify`/`Updates` 都对齐这个写法;`SpecCreate` 的 `StoreIdentity`(`spec_create.go:82`)改为从 token 取。 4. **收口配置类接口**(M2):`get_payment`/`set_payment`/`get_email`/`set_email` 把 `ParseMetaCtx(ctx, nil)` 改成 `RoleValue: "Mall_Admin"`,并把 `identity` 换成 token 中的 `store_identity`(或校验两者相等后拒绝不匹配);`GetSetting` 补上与 `SetSetting` 相同的鉴权;返回 `pay_configs`/`mail_configs` 时对凭据字段做掩码。 5. **`item_fetch` 三处修复**(M7):`item_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-40` 与 `photo_create.go:37-39` 的 `err.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`**:把 `comment`、`nickname` 加进 `Select` 白名单;`comment_re.go:31` 改为 `CommentIdentity: in.GetCommentIdentity()`;`comment_delete.go:28` 去掉第三个 `comment_identity=?` 条件或改用独立入参。 10. **`ads/modify.go:38`、`notice/modify.go:34`**:把 `status` 加进 `Select`,或改用 `map[string]any` 显式指定要更新的列。 11. **占位接口二选一**:`Freight` 的 9 个方法与 `ApplyJoin`/`MiniCode`/`ItemDetailBySpec` 要么按 proto 实现,要么暂时返回 `errcode.ErrUnimplemented`,不要返回 `Code:0 "OK"`;同时把 `freight/remove.go`、`freight/deny_region_remove.go` 接到 `pb.FreightServer` 上(或删除,避免两份实现并存)。 12. **配置与启动**:`config.go:38` 的 `conf.NotNil` 补上 `Databases`;`internal/server/new.go` 初始化 `Mux: gwRuntime.NewServeMux()`,否则独立进程的 HTTP 入口无任何路由。 13. **分页上限**:在 `search.go:34`、`ads/fetch.go:34`、`notice/fetch.go:35`、`comment_fetch.go:38`、`staff/fetch.go:37`、`item_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_fetch`、`spec_create`、`item_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`) | 已修复:统一增加「当前登录者属于该店铺」的归属校验,越权返回 `ErrPermissionDenied`;`SetSetting` 亦从「凭请求 `identity` 直接 `Updates`」改为以 token 中的店铺归属为准,并检查 `RowsAffected`,避免改到他人店铺却静默成功 | | M3 | 高 | 12 处写操作无店铺归属校验(商品/规格/图片/分类/广告/公告/运费/员工) | 已修复:逐处补归属条件(条件中直接带归属字段,`RowsAffected == 0` 即视为越权或不存在) | | M4 | 高 | `Staff.Fetch` 鉴权被注释,员工手机号/邮箱批量泄露 | 已修复:恢复鉴权 | | M5 | 高 | 进货价/代理价经 `ItemDetail`、`SpecDetail`、`SpecFetch` 外泄 | 已修复:匿名或非本店调用者不下发成本类价格字段,本店可正常查看 | | 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/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。