Files
full/docs/mall.md
2026-09-22 18:53:53 +08:00

363 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 客户端"一类改造不在此列。