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

44 KiB
Raw Blame History

order购物车与订单生命周期代码审计报告

内容
审计对象 module/ec/order
服务域 电商-购物车与订单生命周期
审计日期 2026-09-22
代码规模 Go 文件 62 个(pb/ 生成代码 13 个,手写 49 个,手写约 3098 行;含生成代码共 11922 行);proto/ 5 个 618 行;internal/logic/ 25 个文件cart/coupon/mgt/summary/commontest/rpc/ 6 个文件
入口 cmd/maingRPC + HTTP Gateway 单进程)、聚合入口 pkgs/allservice.Expose 复用共享 gRPC/Gateway
对外协议 gRPC + HTTP Gateway路径 /order.<Service>/<Method>,全部为 POST
结论摘要 订单状态推进的 3 个"模拟"接口(支付/发货/收货)在配置中已开启 Gateway 且无角色校验,可被任意已登录账号调用、并把订单推到已支付/已发货;OrderApprove 完全无鉴权。跨服务数据库耦合是结构性问题etc/order_prod.yaml 的 DSN 就是 mall 的库,代码直接读写 mall_product/mall_product_spec/mall_store/address_library。业务侧有 3 处确定性错误会直接产生错误金额或错误库存:优惠券计算把金额加到总额(confirm.go:72)、submit 扣库存不检查 RowsAffectedsubmit.go:209-217)、多店铺下单 store_id 恒为 0submit.go:82,166)。管理端 OrderModify 为假成功占位。

1. 服务定位与职责

围绕一张购物车与一张订单主表,提供两条下单链路(购物车提交 Summary.Submit、单商品快速下单 Summary.QuickCreateByProduct / 管理端 Mgt.OrderCreate)以及订单的查询、确认(运费/优惠券)、取消、售后审批、模拟交易推进。

不负责:商品与规格主数据(module/ec/mall)、收货地址(module/ec/address、运费模板mall 侧未实现)、真实支付与物流对接(本模块用"模拟"接口代替)。

2. 代码结构与入口

路径 职责
cmd/main/main.go 独立进程入口:config.Newimpl.NewImplserver.New(nil)service.Newsrv.Start()
internal/config/config.go 配置结构Base/Databases/MicroService/Rpc/Gateway/Apm/Etcd与端口、密钥初始化
internal/impl/impl.gointernal/impl/with.go Redis / DB / Etcd 初始化;withDatabases 缺少源时直接 panic
internal/models/impl.go 独立的 DB 装配层(models.New/NewMysql/NewPostgres),持有 models.DBServiceAutoMigrate 4 张表
internal/models/*.go 4 个 GORM 模型 + 跨库表的本地投影结构体(Product/Spec/OrderAddress+ 3 个未接线查询函数
internal/logic/common/{no,get,reflect}.go 订单号生成、按 identity/order_no 取订单、模型到 pb 的转换
internal/logic/{cart,coupon,mgt,summary}/** 业务逻辑25 个文件
internal/server/** protoc-gen-slc 生成的 gRPC 服务端桩4 个 server + new.go 注册)
pb/** protoc 生成的 pb / grpc / gateway 代码13 个文件,本次只看接口签名)
service/expose.goservice/dependencies.go 聚合宿主注入:注册 4 组 gRPC + Gateway handler并允许外部覆盖 Redis/Etcd/DB/Cache
proto/*.proto 5 个契约文件const 为公共消息块)
etc/order_{dev,test,prod}.yaml 三套配置,均为 postgres + Port: 12442/Gateway.Port: 12441
test/rpc/*.go 6 个手工联调用例(全部依赖 //go:build integrationBSM_ORDER_TOKEN 环境变量)
README.md 仅一行标题 # orderREADME.md:1-2),无任何说明

依赖注入:service.Dependenciesservice/dependencies.go:12-17)由 pkgs/all 传入共享连接覆盖 impl 全局量(pkgs/all/internal/service/order.go:10-19);注意 applyDependenciesservice/dependencies.go:19-29未处理 Cache 字段

3. 接口清单

共 4 个服务、22 个 RPC全部映射为 POST /order.<Service>/<Method>pb/*.pb.gw.goMustPattern,如 pb/summary.pb.gw.go:728-737)。"鉴权"列取自各 logic 文件实际调用的 service.ParseMetaCtx 参数。

方法 路径 功能 鉴权 实现位置
Fetch /order.Cart/Fetch 购物车列表 token任意角色 logic/cart/fetch.go:16-17
Create /order.Cart/Create 加入购物车 token任意角色 logic/cart/create.go:20-21
Modify /order.Cart/Modify 改数量 token任意角色 logic/cart/modify.go:17-19
Delete /order.Cart/Delete 删除购物车项 token任意角色 logic/cart/delete.go:15-17
ByStatus /order.Coupon/ByStatus 按状态取优惠券 Mall_Admin logic/coupon/by_status.go:16-18
OrderCreate /order.Mgt/OrderCreate 管理端建单 Mall_Admin + Owner 非空 logic/mgt/order_create.go:21-30
OrderModify /order.Mgt/OrderModify 修改订单 Mall_Admin + Owner 非空 logic/mgt/order_modify.go:14-27 占位(假成功)
OrderGet /order.Mgt/OrderGet 订单详情B 端) agency 为空时 Mall_Admin非空时任意 token logic/mgt/order_get.go:19-32
OrderListByStore /order.Mgt/OrderListByStore 店铺订单列表 同 OrderGet logic/mgt/order_list_by_store.go:19-32
OrderCancel /order.Mgt/OrderCancel 取消订单 Mall_Admin + Owner 非空 logic/mgt/order_cancel.go:17-26
OrderReturnable /order.Mgt/OrderReturnable 申请退款/退货 Mall_Admin logic/mgt/order_returnable.go:18-22
OrderApprove /order.Mgt/OrderApprove 售后审批 无任何鉴权 logic/mgt/order_approve.go:17-27
QuickCreateByProduct /order.Summary/QuickCreateByProduct 单商品快速下单 Mall_Admin logic/summary/quick_create_by_product.go:21-26
Submit /order.Summary/Submit 购物车提交下单 address 字段时任意 token否则 Mall_Admin logic/summary/submit.go:24-33
Check /order.Summary/Check 查未付款订单 Mall_Admin logic/summary/check.go:17-22
Get /order.Summary/Get 订单详情C 端) token任意角色 logic/summary/get.go:15-20
List /order.Summary/List 我的订单列表 Mall_Admin logic/summary/list.go:19-24
Confirm /order.Summary/Confirm 确认订单(运费/优惠券) token任意角色 logic/summary/confirm.go:16-21
Cancel /order.Summary/Cancel 取消订单C 端) token任意角色 logic/summary/cancel.go:17-22
SimulatePay /order.Summary/SimulatePay 模拟支付 token任意角色 logic/summary/simulate_pay.go:18-23
SimulateShipments /order.Summary/SimulateShipments 模拟发货 token任意角色 logic/summary/simulate_shipments.go:18-23
SimulateReceiving /order.Summary/SimulateReceiving 模拟收货 token任意角色 logic/summary/simulate_receiving.go:18-23

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

接口 位置 现状
Mgt.OrderModify logic/mgt/order_modify.go:25-33 鉴权与 auth.Owner == nil 判空之后只剩两行 // TODO,直接返回 Code:0 "OK"不修改任何数据internal/server/mgt_server.go:24-26 已把它接到 gRPC 与 Gatewaypb/mgt.pb.gw.go:537),属对外可达的假成功接口。
Summary.SimulatePay logic/summary/simulate_pay.go:17,25 注释即"模拟支付"。真实写库,但交易号、金额、支付类型、备注全为硬编码常量:PayTradeNo: "Pay12371937812897"PayAmount: 10000000100000.00 元)、PayType: 4(余额)、PayRemark: "支付备注",且与订单实际 trans_price 无关。
Summary.SimulateShipments logic/summary/simulate_shipments.go:17,29 注释即"模拟发货"。只写 status=3delivery_identity,不校验当前状态、不产生物流单。
Summary.SimulateReceiving logic/summary/simulate_receiving.go:17,29 注释即"模拟收货"。只把 status 置 4。
配送员信息查询 logic/summary/quick_create_by_product.go:88-97 整段查询 delivery_member 的代码被注释,member_identity 请求参数无任何效果(:119-120 的赋值同样被注释)。
地址与运费计算 logic/summary/confirm.go:36-50 if in.AddressIdentity != "" 分支内只有一段被注释的地址查询与 GetLogisticsFee 调用,函数体为空实现。
models.QuicklyCreateOrder / models.GetSummaryCnt / models.GetSummaryList internal/models/query.go:22internal/models/order_summary.go:70,83 三个模型层函数全仓无调用者,且其中 SQL 引用了模型与表结构中不存在的列(见 6.2)。
README.md module/ec/order/README.md:1-2 只有标题,无接口/部署/数据模型说明。

4. 数据模型与表

本模块自身有 4 张表(internal/models/impl.go:15-20migrateTables 自动迁移),另外直接读写 4 张他服务的表

自有表

表名 模型文件 主键 / 唯一键 说明
order_summary internal/models/order_summary.go:12 id PKidentity 唯一索引;order_no 普通索引store_id/store_identity/status 索引 订单主表
order_details internal/models/order_details.go:9 id PKgorm.Modelsummary_identity/product_identity 索引;order_no 索引 订单明细
order_cart internal/models/order_cart.go:15 id PKidentity 唯一索引;cart_identity 索引;product_identity 索引 购物车,无 passport_identity 唯一性约束
order_coupon internal/models/order_coupon.go:10 id PKidentity 唯一索引 优惠券,归属用 passport_id/passport_identity

order_summaryinternal/models/order_summary.go:12

字段 类型 键/约束 说明
id/created_at/updated_at/deleted_at uint/TIMESTAMP PK + 软删除 gorm.Model
identity varchar(36) uniqueIndex 订单唯一标识UUID
passport_id/passport_identity uint / varchar(36) 索引 下单人(Std_PassportSubmit 写的是当前登录人
order_no varchar(36) 索引(非唯一 18 位订单号,生成见 logic/common/no.go:10-14
store_id/store_identity uint / varchar(36) 索引 店铺(Submit 写入值有缺陷,见 6.2
partner_id int32 default 0 分销商 ID直接取自请求
total_price/trans_price/refund_price/logistics_fee/coupon_amount int64 default 0 订单金额/实付/已退/运费/优惠额(后两项从不被写入,见 6.2
coupon_identity varchar(64) default '' 优惠券标识(全仓从不写入)
remark/args text default '' 备注 / 附加参数
status int32 default 0索引 1 未支付 / 2 已支付 / 3 已发货 / 4 已收货 / 5 已完成 / 6 已收货退款 / 7 未发货退款 / 8 已退货 / -1 已取消
address_identity varchar(36) 地址库标识
county/province/city/area/address/contact/phone varchar(255/50) default '' 下单时冗余落库的收货地址PII
approve int8 default 0 -2 未通过 / 0 默认 / 1 申请退款 / 2 申请退货 / 3 申请退款退货 / 4 申请通过
reason varchar(255) default '' 售后原因
logistics_number/delivery_time/delivery_address/delivery_identity/car_identity/car_brand/car_version/license_number/member_name/member_phone varchar 配送信息;SimulateShipments 只写 delivery_identity
pay_type/pay_amount/pay_trade_no/pay_time/pay_remark int8/int64/varchar/time/text default 支付信息;SimulatePay 全部硬编码
order_details foreignKey:SummaryIdentity;references:Identity 关联明细(order_summary.go:61

order_detailsinternal/models/order_details.go:9

字段 类型 键/约束 说明
id uint PK 自增主键
summary_identity varchar(36) Index 所属订单标识
order_no varchar(36) indexnot null 订单号(冗余)
product_id/product_identity int64 / varchar(36) not null / Index 商品
spec_id int64 指向 mall_product_spec.id(跨库外键语义)
type/title/spec_title/spec_no/cover_image/product_args int8/varchar/text default 类型、标题、规格名、规格编号、封面、参数
number int32 default 0 数量,Submit 中为 0 时跳过扣库存
unit_price/sales_price/total_price int64 default 0 实际单价/原价/小计(单位分)
gas_types int32 default 1 燃气类型
supply_id int64 default 0 供应商 ID来自跨库商品表

order_cartinternal/models/order_cart.go:15

字段 类型 键/约束 说明
id uint PK 自增主键,Cart.Modify/Cart.Delete 直接当作唯一入口条件
identity varchar(36) uniqueIndex 购物车行标识
cart_identity varchar(36) indexnot null 购物车分组标识(来自请求,未登录购物用)
passport_id/passport_identity uint / varchar(36) 索引 归属用户(Cart.Create 只写 passport_identity:43
product_id/product_identity/spec_id int64/varchar(36)/int64 索引 商品与规格(spec_id 指向 mall_product_spec.id
number int32 default 0 数量(无正值校验)
product_args text default '' 下单前由前端回传的商品参数快照

直接读写的外部表(跨服务数据库耦合)

归属模块 order 侧的读写位置
mall_product module/ec/mall 读:logic/summary/submit.go:82,112logic/summary/quick_create_by_product.go:51,53logic/mgt/order_create.go:57
mall_product_spec module/ec/mall 读:submit.go:123,204quick_create_by_product.go:68order_create.go:67submit.go:209stock - n)、order_create.go:141stock - n)、order_approve.go:60stock + n
mall_store module/ec/mall 读:quick_create_by_product.go:35
address_library module/ec/address 读:submit.go:68quick_create_by_product.go:79order_create.go:101
delivery_member 未在本仓库中找到对应模型 读:quick_create_by_product.go:90(整段被注释)

两侧数据库连接配置指向同一个库:etc/order_prod.yaml:7module/ec/mall/etc/mall_prod.yaml:7 的 DSN 均为 dbname=ec_mall。order 侧这些查询用 Table("...") 加本地投影结构体(internal/models/query.go:3Product:16Specinternal/models/types.go:7OrderAddress),没有 GORM 模型、没有编译期约束。

5. 核心流程

flowchart TD
    A["POST /order.Summary/Submit"] --> B["ParseMetaCtx带 address 时任意 token"]
    B --> C["按 passport_identity 取整辆购物车"]
    C --> D{"请求带回 address"}
    D -->|是| E["直接用请求里的地址字段"]
    D -->|否| F["按 address_identity 直查 address_library不校验归属"]
    E --> G["逐项直查 mall_product / mall_product_spec"]
    F --> G
    G --> H["按 store_identity 分组,用库里的 spec.price 计算金额"]
    H --> I["事务:写 order_summary + order_details"]
    I --> J["逐条 stock >= n 条件更新 mall_product_spec 库存(不检查影响行数)"]
    J --> K["按 passport_identity 清空购物车"]
    K --> L["返回首个 order_no"]
flowchart TD
    A["POST /order.Summary/SimulatePay"] --> B["ParseMetaCtx(nil):任意已登录角色"]
    B --> C["按请求传入的 identity 列表批量更新 order_summary"]
    C --> D["status=2, pay_type=4, pay_amount=10000000, pay_trade_no=Pay1237...897"]
    E["POST /order.Summary/SimulateShipments"] --> F["ParseMetaCtx(nil) + member_identity 非空"]
    F --> G["status=3, delivery_identity=入参"]
    H["POST /order.Summary/SimulateReceiving"] --> I["ParseMetaCtx(nil)"]
    I --> J["status=4"]
    K["POST /order.Mgt/OrderApprove"] --> L["无鉴权"]
    L --> M["按 approve 分支改 status 或回补 mall_product_spec.stock"]

6. 审计发现

6.1 安全

级别 位置 问题
logic/summary/simulate_pay.go:20,25logic/summary/simulate_shipments.go:20,29logic/summary/simulate_receiving.go:20,29 模拟支付/发货/收货在生产可达且无角色校验。三个 RPC 在 proto/summary.proto:33,36,39 声明、internal/server/summary_server.go:53-66 已注册、pb/summary.pb.gw.go:735-737 暴露为 POST 路由;实现只用 service.ParseMetaCtx(ctx, nil)(只验 token 有效性,不做角色判断,见 bsm-sdk/core/service/meta.go:36-45),随后按请求传入的 identity 列表直接改状态。任何已登录账号(含 role=staff)都能把任意订单标记为已支付、已发货、已收货。etc/order_prod.yaml:23Gateway.Enable: trueSDK 网关绑定 0.0.0.0bsm-sdk/core/service/service.go:106),生产网络直接可达。
logic/summary/simulate_pay.go:25 支付信息硬编码:PayTradeNo: "Pay12371937812897"PayAmount: 10000000PayType: 4(余额)、PayRemark: "支付备注"。金额与订单 trans_price 完全无关,等于以任意金额标记已支付,账实不符。
logic/mgt/order_approve.go:17-27 OrderApprove 没有任何 ParseMetaCtx 调用:匿名即可审批退款/退货,并触发 case 2 的库存回补(:60)。
logic/summary/get.go:17,27 + logic/common/get.go:15 Get 只验 tokenGetOrderSummaryByIdentityidentity 直查,不校验订单与调用方的关系(既非 passport_identity 也非 store_identity)。任意已登录用户可读任意订单,返回体经 logic/common/reflect.go:58-64 带出省市区详细地址、联系人、电话。
logic/mgt/order_get.go:21-24,39-46logic/mgt/order_list_by_store.go:21-24,52-57 两处的 agency 分支在请求带 agency 时把 ParseOptions 置为 nil任意角色可过并把 store_identity 直接取请求参数order_get.go:46 的查询只有 store_identity + identity 两个条件 → 可读任意店铺订单。
logic/mgt/order_cancel.go:37logic/summary/cancel.go:28,41logic/mgt/order_returnable.go:19,49 都只判角色、不判订单归属:OrderCancelidentity 直改 status=-1OrderReturnableidentity 直改 approve/reasonCancelorder_no 查询与更新。同角色可操作他店订单。
logic/cart/create.go:27,49logic/cart/modify.go:26,40logic/cart/delete.go:22 购物车写操作缺归属约束:Cart.Create 的查重与更新条件是 cart_identity + product_identity + spec_id(不带 passport_identity),命中后 Updates(&data) 会把该行的 passport_identity 改成调用者(:43,49Cart.Modify/Cart.Delete 只按 id 定位(modify.go:26,40delete.go:22id 为自增主键可枚举。
logic/cart/fetch.go:31 Where("cart_identity=? or passport_identity=?", in.GetCarIdentity(), auth.Identity)or 语义使传入任意 cart_identity 即可读到他人的购物车,与当前账号无关。
logic/summary/submit.go:68logic/summary/quick_create_by_product.go:79logic/mgt/order_create.go:101 三处都按 identity 直查 address_library从不校验 owner_identity/owner_id(该列确实存在于 module/ec/address/internal/models/address_library.go:17)。可用任意地址标识下单,并把他人姓名、电话、详细地址写入订单(address["contact"].(string)address["detail"].(string) 等,order_create.go:115-121)。
logic/cart/fetch.go:57 UnitPrice: product.CostPrice——购物车返回体把成本价当作"实际单价"给前端(:40-47 的子查询只取了 cost_price 与最低销售价)。
logic/coupon/by_status.go:16-28 鉴权到位但业务越权面未收敛:Where("passport_id=?", auth.ID) 已按归属过滤(正确);但 logic/summary/confirm.go:56identity 取券时既不校验 passport_id/passport_identity,也不校验 started/expired 有效期与 condition 门槛(字段见 internal/models/order_coupon.go:13,17-19)→ 可用他人券、过期券、未达门槛券。
logic/cart/create.go:37-41logic/cart/modify.go:26,36-40 number 无正值校验,可写入 0 或负数;负数数量在 submit.go:202detail.Number > 0 判假而跳过扣库存,直接生成负数量明细与错误的金额。
internal/impl/with.go:85 NewPostgres 中硬编码 db = db.Debug()NewMysql 是按 options.Debug 判断,:61-63),生产环境同样输出含参数的 SQL 日志。
logic/summary/submit.go:206 库存不足时用标准库 log.Printf 输出,与同文件其它位置的 printer.Error:47 等)混用,日志格式与级别不受统一管控。

6.2 正确性与逻辑缺陷

级别 位置 问题
logic/summary/confirm.go:60,71-73 优惠券把金额加到总额couponAmount = coupon.Amount 之后 summary.TotalPrice = summary.TotalPrice + couponAmount。用券后应付金额变大。同时从不回写 summary.CouponIdentity/CouponAmount(全仓仅 cancel.go:36-37 清零、order_summary.go:26-27 定义),订单上看不到用了哪张券、抵了多少。
logic/summary/cancel.go:48 取消订单要按 summary.CouponIdentity 把券置回可用,但该字段从未被赋值 → Where("identity=?", "") 更新不到任何行,券永远停在 status=3confirm.go:59Update 也未检查 RowsAffected
logic/summary/submit.go:200-218 超卖:先 Scan(&stock) 试算(:204),再用 Where("id = ? AND stock >= ?", ...).UpdateColumn("stock", stock - n):209-212),但从不检查 RowsAffected。并发下条件更新影响 0 行时不会报错,订单仍然提交成功、库存未扣。对比 logic/mgt/order_create.go:150-152 是正确写法(有 RowsAffected == 0 判断)。
logic/summary/submit.go:82,91-93,166 Select("store_identity") 未取 id,而 :92 用的是 product.StoreId → 该值恒为 0且只在 k == 0 时赋值一次。多店铺下单时,所有订单摘要写入同一个 store_id0 或首店 ID店铺维度统计与后续按店查询全部失真。
logic/summary/submit.go:45,221-225 忽略 SubmitRequest.idproto/summary.proto:70 明确注释为"购物车的ID数据"):下单按 passport_identity 取全车,成功后按 passport_identity 清空全车。用户勾选部分商品下单会下整辆购物车,其余条目被删除。
logic/summary/quick_create_by_product.go:128,138logic/mgt/order_create.go:75,77 单价取自 mall_product.sales_price,而 mall 侧的商品创建/更新逻辑从不写这一列module/ec/mall/internal/logic/product/item_create.go:137-160itemToModel 未映射 SalesPrice,该列仅有模型定义 mall_product.go:40)→ 实际为默认 0快速下单与管理端建单的成交金额恒为 0mall 侧仅在排序里读它(module/ec/mall/internal/logic/product/item_fetch.go:73)。
logic/summary/cancel.go:34-53logic/mgt/order_cancel.go:37 取消订单不返还库存:库存在 submit.go:209-211 / order_create.go:141-143 已扣减,取消时只改状态。summary/cancel.go:34 还在 status != 1静默返回 Code:0 "OK",既不取消也不报错。
logic/mgt/order_cancel.go:37logic/summary/simulate_pay.go:25logic/summary/simulate_shipments.go:29logic/summary/simulate_receiving.go:29 订单状态机无任何约束:均可从任意状态跃迁(已发货→已取消、已取消→已支付、未支付→已收货)。summary/cancel.go:34 是唯一有状态判断的实现(但失败被静默吞掉)。
logic/summary/quick_create_by_product.go:102,128,138 同一订单内金额口径不一致:trans_price/total_price 用规格价 spec["price"]:102),明细 unit_price 用商品价 product["sales_price"]:128,138)→ 订单总额与明细之和不相等;logic/mgt/order_create.go:75,77,87 同样用商品价而 SpecID 取自另一条规格(:90)。
logic/summary/quick_create_by_product.go:50-53,68 规格按 product_identity 取第一条(:68,用 Take,请求里的 spec_identity 未被使用);而特殊商品分支(serial_idgas_by_kg/gas_by_bottle)在 :51 用序列号找到商品后,:68 仍用 in.ProductIdentity(即 gas_by_kg 这类字符串)去查 product_identity → 必然 not found特殊商品无法下单。:53 的商品查询也未带 store_identity 条件(仅 :51 分支有),可给 A 店订单塞 B 店商品。
logic/summary/quick_create_by_product.go:149-159 订单摘要与明细分两次独立 Create没有事务:明细写入失败会留下无明细的订单主记录。对比 submit.go:187-228order_create.go:131-156 都用了事务。
logic/mgt/order_approve.go:47-67 impl.DBService.Transaction(func...) 的返回值被丢弃(:47 未接收 error事务失败对外仍返回成功退货入库用 Where("product_identity = ?", specs.ProductIdentity):60)——按商品维度给该商品下所有规格加库存(一个商品多条规格即库存虚增),而正确目标是订单明细里的 spec_id
logic/summary/confirm.go:24,36-53logic/summary/submit.go:168 运费与备注不落库:logisFee 唯一赋值在被注释的分支内(:47),恒为 0if logisFee > 0:51)永不成立;ConfirmRequest.logistics_feeproto/summary.proto:112)与 remark:111)从未被使用;LogisticsFee 全仓只在 submit.go:168 写 0。
logic/mgt/order_returnable.go:40-41 用裸 errors.New("订单状态异常") 返回(非 errcodegRPC 侧表现为 Unknown状态判断只排除 -1/6/7/8已完成的 status=5 也可再次申请售后。
logic/coupon/by_status.go:28,52 ByStatus 完全忽略入参 statusStatus.statusproto/coupon.proto:13)→ 查询恒返回该用户全部券;返回体 Amount: v.Intro 把描述串当金额(CouponItem.amount 是 stringproto/coupon.proto:25started/expired/status 三个字段未填充。
logic/summary/check.go:23-30 注释是"检测是否有未确认及付款的订单",实现是按 passport_id第一条订单且无任何状态过滤(:26);查不到时返回 ErrDB 而非明确的"无订单"。
logic/mgt/order_get.go:46-50 Find 而非 First:查不到不返回错误,:48-50gorm.ErrRecordNotFound 分支是死分支,接口返回空的 summary 而非 404。
logic/summary/submit.go:45-52,195 空购物车也走通全流程:summary/details 为空切片时 CreateInBatches(details, len(details))batchSize 为 0:195),接口仍返回 Code:0order_no 为空字符串。
internal/models/order_summary.go:72,86,131-138 GetSummaryCnt/GetSummaryList 使用 typebuyer_identitymerchant_identitysign_statuspay_statuslogistics_statusinvoice_statusOrderSummary 模型中不存在的列(模型字段见 :12-62),这些查询必然报 Unknown column。二函数与 internal/models/query.go:22 QuicklyCreateOrder 均无调用者。:131 还把 maxPrice 误传给了 pay_status 的条件。
internal/models/query.go:23,86 这两个函数用的是 models.DBService,而全部业务逻辑用的是 impl.DBService(赋值见 internal/impl/with.go:46)。聚合入口经 service/dependencies.go:27 只覆盖 impl.DBServicemodels.DBService 保持 nil → 一旦这些函数被调用即空指针。
logic/summary/submit.go:55-76 in.Address != nil 时才用请求内地址,否则查地址库;但 models.OrderAddressinternal/models/types.go:7-18)含 Email/ZipCode 字段,而 order_summary 没有对应列,这两项落不了库。

6.3 未完成实现

见第 3 节末尾表格。要点:

  • 管理端改单为假成功logic/mgt/order_modify.go:25-33 只有 TODO返回 Code:0 "OK",但接口已在 internal/server/mgt_server.go:24pb/mgt.pb.gw.go:537 接线,调用方会误以为改单成功。
  • 三个"模拟"接口是唯一的状态推进手段simulate_pay.go:17,25simulate_shipments.go:17,29simulate_receiving.go:17,29 注释自述为模拟,硬编码交易号/金额/支付类型,但没有真实支付与物流对接的实现存在。
  • 注释掉的代码块quick_create_by_product.go:88-97(配送员查询)、:119-120DeliveryIdentity 赋值)、confirm.go:37-49(地址查询与运费计算),对应的请求字段 member_identitydelivery_timelogistics_feeremark 因此无效果。
  • 状态语义只在注释里order_summary.go:30(订单状态)与 :39approve 编码)、order_approve.go:29 的取值说明都没有枚举定义,order_returnable.go:24 又硬编码了 1/2/3
  • README 未编写module/ec/order/README.md:1-2 只有标题。

6.4 健壮性与可维护性

级别 位置 问题
service/dependencies.go:19-29 + pkgs/all/internal/service/order.go:15 Dependencies 声明了 Cache *cache.CacheapplyDependencies 却不处理该字段;pkgs/all 仍传入 Cache: impl.MemoryService → 依赖注入字段无效(与 module/base/logs 同类问题)。
internal/impl/with.go:24-31 config.Spec.Cache == ""RedisCache 为 nil:30 无条件取 RedisCache.DB → 空指针;且 printer.Info 的格式化参数与占位符不符。
logic/summary/quick_create_by_product.go:43,62,102,104,112-117,128-129,133,136-137,140,143-145logic/mgt/order_create.go:62,74-75,83,85-86,89-94,115-122logic/mgt/order_get.go:42-43logic/mgt/order_list_by_store.go:55-56 大量对 GORM map[string]any 结果的无保护类型断言store["status"].(int16)product["status"].(int32)spec["price"].(int64)address["province"].(string) 等)。相关列在两边都有 default没有 not null(如 mall_store.status 定义于 bsm-sdk/core/types/db.go:34),任一列为 NULL 或两侧驱动返回类型变化即 panicorder_get.go:42-43 在 token 的 owner 不是对象时同样 panic。【信息不足】未在真实数据库确认各列的可空性与驱动返回的具体类型。
全模块 无单元测试:test/rpc/*.go 均为 //go:build integration 的手工联调用例(summary_test.go:1cart_test.go:1),依赖真实服务端口 127.0.0.1:12442 与环境变量 BSM_ORDER_TOKENtest/rpc/bisic_test.go:13,15,17),断言只打印返回消息,无任何 t.Fatal 判断;logic/models/internal/impl/ 0 测试。
internal/logic/common/no.go:10-14 + internal/models/order_summary.go:18 订单号 = 秒级时间戳拼接 + 6 位随机数,order_no 只有普通索引(非唯一)→ 同秒并发存在重号可能;而 summary/cancel.go:28summary/confirm.go:30common/get.go:28 都用 First(取任意一条),重号会导致操作到错误订单。
logic/summary/list.go:43-45logic/mgt/order_list_by_store.go:49-51 只要 page_size > 0 就直接 Limit(int(in.PageSize))没有上限OrderListByStore 还预加载了全部明细(:60 Preload("OrderDetails"))→ 一次请求可拉走全表订单与明细。
internal/models/impl.go:31,36,42 驱动不支持、DB 打开失败、AutoMigrate 失败三处都走 log.Fatalln(直接退出进程),与 internal/impl/with.go:35,44,53panic 风格并存,启动期错误处理方式不统一。
etc/order_{dev,test,prod}.yaml:10 Cache: redis://null:...@127.0.0.1:6379/(无 DB 序号),而 internal/impl/with.go:30 会打印 RedisCache.DBRedisCache 除该打印外全模块无使用点(死代码)。
etc/order_{dev,test,prod}.yaml:19 Anonymous: - order.ping.hello,但 order 的 proto 与 gw 路由(pb/*.pb.gw.go 共 22 个方法)中没有 ping,是死配置。
logic/mgt/order_cancel.go:29-36logic/mgt/order_get.go:34-37 重复的参数校验(in.Identity == "" 判断两次),order_cancel.go:29:34 的注释还互相错位。
internal/models/impl.go:85 NewPostgresdb = db.Debug()NewMysql 的可选 Debug 行为不一致(详见 6.1)。

7. 风险汇总

编号 级别 问题 影响面
O1 模拟支付/发货/收货在生产可达、无角色校验、金额硬编码 资金与订单状态被任意篡改
O2 OrderApprove 完全无鉴权 退款/退货被匿名审批 + 库存被改写
O3 跨服务直连同库,直接读写 mall_product/mall_product_spec/mall_store/address_library 任一表结构变更即 order 运行期报错;订单侧可直接改商品库存
O4 优惠券把金额加到订单总额(confirm.go:72 金额计算错误,用户应付变高
O5 submit 扣库存不检查 RowsAffected 超卖(订单成功但未扣库存)
O6 多店铺下单 store_id 恒为 0 订单归属错误,店铺维度统计/查询失真
O7 Submit 忽略选中条目、清空全车 用户被强制下整辆购物车
O8 快速下单/管理端建单单价取自从不写入的 sales_price 成交金额恒为 0
O9 取消订单不返库存 + 状态机无约束 + 取消失败静默成功 库存丢失、状态错乱
O10 订单详情越权(Summary.Get、地址越权使用4 处)、购物车越权(读/写/删) 用户 PII 泄露与数据被篡改
O11 购物车返回成本价(cart/fetch.go:57 商业成本信息泄露
O12 优惠券归属/有效期/门槛均不校验 券被滥用
O13 无事务的建单(quick_create_by_product 两次 Create、事务错误被丢弃order_approve.go:47 脏数据
O14 退货入库按 product_identity 回补 库存虚增
O15 运费/备注/优惠额字段全链路未落库 订单金额要素缺失
O16 分页无上限 + Preload 全明细 服务被单请求拖垮
O17 无保护类型断言遍布订单逻辑 运行期 panic
O18 无单元测试、双份 DB 全局、订单号无唯一约束 可维护性与数据一致性
O19 README 空缺、Anonymous 死配置、RedisCache 死代码、Debug 常开 认知成本与日志噪音

8. 修复建议(务实项)

  1. 物理下线模拟接口O1SimulatePay/SimulateShipments/SimulateReceivingproto/summary.proto:33,36,39 移除或改为仅内部注册(不挂 Gateway在实现落地前最直接的做法是三个方法体统一返回 errcode.ErrUnimplemented,并把 logic/summary/simulate_pay.go:25 的硬编码 PayTradeNo/PayAmount 删除。若必须保留,至少改成 ParseOptions{RoleValue: "Mall_Admin"} 并在更新前加 status = 1(支付)/status = 2(发货)/status = 3(收货)的条件与订单归属校验。
  2. OrderApprove 补鉴权与归属校验O2order_approve.go:17 前加 service.ParseMetaCtx(ctx, &service.ParseOptions{RoleValue: "Mall_Admin"}):24:34/:48/:73/:86Wherestore_identity(取自 token owner:47 接收 Transaction 的返回值并判断;:60Where("product_identity = ?") 改为 Where("id = ?", specs.SpecID)
  3. 优惠券计算改减并落库O4、O15confirm.go:72 改为 summary.TotalPrice - couponAmount(并加下限判断);同时把 summary.CouponIdentity = in.CouponIdentitysummary.CouponAmount = couponAmount 写入,这样 cancel.go:48 的退券逻辑才能生效;:56 取券时补 passport_idstarted/expiredcondition 校验。
  4. 库存扣减与返还O5、O9、O14submit.go:209-217 参照 order_create.go:150-152 增加 if result.RowsAffected == 0 { return errors.New("库存不足") },并删掉 :204 的预读(条件更新已足够);summary/cancel.go:34-53mgt/order_cancel.go:37 在状态置 -1 的同事务内按 order_details.spec_id 回补库存;cancel.go 在状态不允许取消时应返回错误而不是 Code:0
  5. 修正 Submit 的三处错误O6、O7submit.go:82Select 补上 id;删除 :91-93k == 0 逻辑,改为按 store_identity 分组后就地取该组商品的店铺 ID:45 改为按 in.GetId() 过滤购物车(为空时再退化为整车),:221-225 只删除已下单的那些 order_cart.id:45 之后加 len(cart) == 0 的显式报错。
  6. 金额口径统一O8、O13quick_create_by_product.go:102,128,138order_create.go:75,77,87 统一使用 mall_product_spec.pricequick_create_by_product.go:68 已取到 specorder_create.go:67 的规格查询改为按请求传入的规格标识(SpecList 增加 spec_id/spec_no)而不是 product_identity 取任意一条;quick_create_by_product.go:149-159 的两条 Create 包进 DBService.Transaction
  7. 地址归属校验O10submit.go:68quick_create_by_product.go:79order_create.go:101 的地址查询补 owner_identity = ?(取自 token并在 :79 处把 in.AddressIdentity 与查询结果一并校验;summary/get.go:27 按调用方身份收敛查询条件(买家用 passport_identity,商家用 store_identity)。
  8. 购物车归属收敛O10、O11cart/modify.go:26,40cart/delete.go:22Wherepassport_identity = ?cart/create.go:27,49 的查重条件补 passport_identitycart/fetch.go:31or 改为按 passport_identity 过滤、cart_identity 仅作附加条件;cart/fetch.go:57UnitPrice 改用 product.SalesPrice,不要返回 CostPrice
  9. 输入校验O12cart/create.go:39cart/modify.go:26,40 增加 number > 0 校验;order_returnable.go:40 改用 errcode
  10. 降低跨库耦合的运行期风险O3至少把 order 侧对 mall_product/mall_product_spec/mall_store/address_library 的查询集中到 internal/models/query.go 一处(当前散落在 8 个文件),并给每个投影结构体补注释说明依赖的列名与归属服务;etc/order_prod.yaml:7 的 DSN 与 mall 指向同一库这一点应在配置注释中显式写明,避免被误改为独立库。
  11. 分页与日志O16、O19summary/list.go:43-45mgt/order_list_by_store.go:49-51 增加 if size > 200 { size = 200 }internal/impl/with.go:85 去掉硬编码 .Debug()submit.go:206 改用 printer.Error
  12. 类型断言防抖O17quick_create_by_product.goorder_create.goorder_get.goorder_list_by_store.go 中所有 .(T) 改为逗号断言(v, ok := m["k"].(T))并在 !ok 时返回 errcode.ErrDB/ErrRecordNotFound
  13. 订单号与约束O18internal/models/order_summary.go:18order_no 改为 uniqueIndex,并在 Submit/OrderCreate 生成冲突时重试;internal/models/query.go:23,86 若确定不用则删除,要用则改为 impl.DBService
  14. 清理与测试:删除 internal/models/order_summary.go:70-151internal/models/query.go:22 三个坏 SQL 函数(或按模型重写后接线);删掉 order_cancel.go:29-36 的重复校验与 etc/*.yaml:19order.ping.hello;为 Submit(多店铺、空车、库存不足、优惠券)、ConfirmCancelCart 各补 1 条正例 + 1 条边界用例(现有 test/rpc/*.go 只打印返回,无断言);补写 README.md

本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。仓储里"把商品查询改成 RPC 调用""引入订单状态机框架""引入领域事件保证最终一致""增加 DTO/VO 层"一类改造不在此列;上述第 10 条针对的是运行期约束缺失这一实际问题,最小动作是集中查询位置并注明依赖,不是新建抽象层。

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属本机既有问题

编号 级别 问题 处理结果
O1 模拟支付/发货/收货在生产可达、无角色校验、金额硬编码 已修复:补 service.ParseMetaCtx 鉴权,并加运行模式开关(仅 dev 可执行,非 dev 直接拒绝),硬编码支付参数改为从配置读取
O2 OrderApprove 完全无鉴权 已修复:补鉴权,并校验审批值与订单当前状态,非法状态跃迁被拒绝
O3 跨服务直连同库(mall_product/mall_product_spec/mall_store/address_library 部分处理:架构现状保留(未做服务化改造);本次只补齐必要过滤条件、检查全部 .Error、对非本用户数据的写入补归属校验
O4 优惠券把金额加到订单总额 已修复:改为减法,并补归属/有效期/使用门槛校验(仅使用模型实际存在的字段)
O5 扣库存不检查 RowsAffected(超卖) 已修复:改为原子条件更新 stock = stock - ? WHERE id = ? AND stock >= ?RowsAffected == 0 即回滚事务并返回库存不足
O6 多店铺下单 store_id 恒为 0 已修复:store_id 改从商品/店铺明细的实际归属取,订单归属正确
O7 Submit 忽略选中条目、清空全车 已修复:只处理请求中选中的购物车条目,未结算条目保留
O8 快速下单/管理端建单单价取自从不写入的 sales_price,成交金额恒为 0 已修复:改用模型实际存在的价格列,成交金额不再为 0
O9 取消订单不返库存 + 状态机无约束 + 取消失败静默成功 已修复:取消时在事务内回补库存;校验当前状态是否允许取消;检查所有 .Error,不再静默成功
O10 订单详情越权、地址越权使用4 处)、购物车读/写/删越权 已修复:全部补 auth.Identity 归属校验,越权返回 ErrPermissionDenied
O11 购物车返回成本价 已修复:响应不下发成本价;同文件吞掉查询错误的 if err == nil 一并修掉

未纳入本轮范围

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

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