Files
full/docs/mall.md

388 lines
46 KiB
Markdown
Raw Normal View History

2026-09-22 18:53:53 +08:00
# 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.<Service>/<Method>`,全部为 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.<Service>/<Method>``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) | 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_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.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"]
```
```mermaid
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` | **鉴权代码被注释**`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 客户端"一类改造不在此列。
2026-09-22 21:15:34 +08:00
## 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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。