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

42 KiB
Raw Permalink Blame History

wallet钱包与支付接入代码审计报告

内容
审计对象 module/finance/walletmodule 路径 bsm/full/module/finance/wallet
服务域 资金与支付
审计日期 2026-09-22
代码规模 手写 Go 45 个文件 / 2382 行;生成代码 pb/ 13 个文件 / 10075 行;proto/ 5 个文件 / 684 行;etc/ 3 个 yaml + 1 个 supervisor conf*_test.go 0 个
入口 独立入口 cmd/main/main.gogRPC + grpc-gateway 单进程,端口 12238/12239聚合入口 pkgs/allpkgs/ecmall(经 service.Exposecmd/cli/main.go 为占位 CLI
对外协议 gRPC4 个 service / 23 个 RPC+ grpc-gateway RESTPOST /{proto包名}.{服务}/{方法}proto/*.proto没有任何 google.api.http 注解,路径由 protoc-gen-slc 生成
结论摘要 分层与目录照模板齐备,但资金链路端到端不通:唯一入账函数 ChargeWallet 全仓库无调用点,支付宝分支必然 panic微信/支付宝共 7 个 RPC 为 TODOPayment.Callback 只需用户 JWT 即可把支付单置为成功;提现只校验不扣减、余额扣减为读后写且不写流水、支付密码可被直接覆盖。生产/测试配置因字段名不匹配而无法加载。

1. 服务定位与职责

面向 C 端用户的钱包主体:用户钱包(余额、可提现余额、支付密码、绑定的微信/支付宝标识)、银行卡绑定、交易流水、充值/订单支付下单、提现申请以及对微信支付APIv3与支付宝的渠道接入。

它是资金账户与支付下单服务。不负责订单主体生命周期(订单金额从 order_summary 表直接读取,internal/models/query.go:104)、不负责结算清算、也没有对账/退款实现(wallet_refund 表只有模型与迁移注册,无任何代码引用)。

2. 代码结构与入口

路径 职责
cmd/main/main.go 独立进程入口:config.New("Wallet")impl.NewImpl()server.New(nil)service.Newsrv.Use(models.InitData)srv.Start()
cmd/cli/main.go 占位 CLI只打印 Hello World! / Success
internal/config/config.go SrvConfigBase、Databases、MicroService、Rpc、Gateway、APM、Etcd、WeChat、Alipay、Wallet、QrCodeSavePath与校验
internal/impl/impl.go 初始化 Memory、Redis、DB、Etcd 四个全局实例
internal/excode/ex.go 钱包业务错误码3801~3821
internal/server/{new,payment_server,basic_server,wechat_server,alipay_server}.go gRPC 服务注册与 1:1 转发到 logic 层(protoc-gen-slc 生成)
internal/logic/basic/{wallet,get_wallet,set_pay_password,bind_payment_id,transactions,add_bank_card,get_bank_card,rm_bank_card,apply_cash}.go 钱包/密码/银行卡/流水/提现
internal/logic/payment/{hello,way,get,by_order,by_charge,callback}.go 支付网关列表、下单、支付详情、回调
internal/logic/wechat/{wechat,jsapi_pre_order,app_pre_order,native_pre_order,transfer,wx_callback}.go 微信 APIv3 客户端与下单/回调
internal/logic/alipay/{alipay,wap_pay,page_pay,app_pay,transfer}.go 支付宝客户端与支付/转账
internal/models/{wallet_basic,wallet_payment,wallet_record,wallet_apply_cash,wallet_bank,wallet_refund,query}.go 6 张表的模型 + GORM 自动迁移注册 + query.go 中的全部数据访问函数
service/{expose,dependencies}.go 聚合宿主注入用:Expose 负责注册 gRPC/gateway handlerDependencies 支持外部覆盖 Redis/Etcd/DB/Cache
proto/*.proto 4 个 service 契约(无 HTTP 注解)
etc/{wallet_dev,wallet_prod,wallet_test}.yamletc/supervisor.bsm-finance-wallet.conf 配置与进程守护配置

依赖注入:service.Exposeservice/expose.go:22-43)先 applyDependencies 覆盖 internal/impl 中的全局对象,再注册 4 个 gRPC service 与 4 组 gateway handler。独立入口不走该路径直接使用 internal/impl 自行初始化的对象。

3. 接口清单

全部 RPC 的 HTTP 映射均为 POST /{proto包名}.{服务}/{方法},路径由生成代码固定(pb/wallet.pb.gw.go:600-607pb/payment.pb.gw.go:472-477pb/wechat.pb.gw.go:408-412pb/alipay.pb.gw.go:344-347)。

方法 路径 功能 鉴权 实现位置
POST /wallet.Basic/GetWallet 钱包信息 + 六类收支统计 JWT logic/basic/get_wallet.go:18
POST /wallet.Basic/SetPayPassword 设置/覆盖支付密码 JWT logic/basic/set_pay_password.go:17
POST /wallet.Basic/BindPaymentId 绑定微信/支付宝账户标识 JWT logic/basic/bind_payment_id.go:17
POST /wallet.Basic/Transactions 交易流水分页查询 JWT logic/basic/transactions.go:15
POST /wallet.Basic/AddBankCard 添加银行卡 JWT logic/basic/add_bank_card.go:18
POST /wallet.Basic/GetBankCard 银行卡列表 JWT logic/basic/get_bank_card.go:16
POST /wallet.Basic/RmBankCard 删除银行卡 JWT logic/basic/rm_bank_card.go:17
POST /wallet.Basic/ApplyCash 提现申请 JWT logic/basic/apply_cash.go:19
POST /wallet.Payment/Hello 支付服务健康探测 JWT logic/payment/hello.go:13
POST /wallet.Payment/Way 平台可用支付方式列表 JWT logic/payment/way.go:14
POST /wallet.Payment/Get 支付单详情 JWT按 passport 过滤) logic/payment/get.go:13
POST /wallet.Payment/ByOrder 订单支付下单 JWT logic/payment/by_order.go:21
POST /wallet.Payment/ByCharge 充值下单 JWT logic/payment/by_charge.go:20
POST /wallet.Payment/Callback 更新支付单结果与状态 JWT仅按 passport 过滤) logic/payment/callback.go:15
POST /wallet.Wechat/JsapiPreOrder 微信 JSAPI 预下单 (未调用 ParseMetaCtx logic/wechat/jsapi_pre_order.go:10
POST /wallet.Wechat/AppPreOrder 微信 APP 下单 JWT 占位 logic/wechat/app_pre_order.go:11
POST /wallet.Wechat/NativePreOrder 微信 Native 下单 JWT 占位 logic/wechat/native_pre_order.go:11
POST /wallet.Wechat/Transfer 微信转账到零钱 JWT 占位 logic/wechat/transfer.go:11
POST /wallet.Wechat/WxCallback 微信支付回调 无 JWT使用微信平台验签 logic/wechat/wx_callback.go:15(业务逻辑为 TODO
POST /wallet.Alipay/WapPay 支付宝手机网站支付 JWT 占位 logic/alipay/wap_pay.go:11
POST /wallet.Alipay/PagePay 支付宝电脑网站支付 JWT 占位 logic/alipay/page_pay.go:11
POST /wallet.Alipay/AppPay 支付宝 APP 支付 JWT 占位 logic/alipay/app_pay.go:11
POST /wallet.Alipay/Transfer 支付宝转账 JWT 占位 logic/alipay/transfer.go:11

鉴权方式:除 Wechat.WxCallback 外,所有 logic 入口均调用 service.ParseMetaCtx(ctx, nil)git.apinb.com/bsm-sdk/core/service/meta.go:19-49),要求 metadata 中携带可解析的 JWT没有做角色/私有 IP 校验(opts 恒为 nil)。

声明但未实现 / 空转的接口

类别 接口 证据
TODO 占位(只做鉴权,随后直接 return Wechat.AppPreOrder internal/logic/wechat/app_pre_order.go:18-22
同上 Wechat.NativePreOrder internal/logic/wechat/native_pre_order.go:18-22
同上 Wechat.Transfer internal/logic/wechat/transfer.go:18-22
同上 Alipay.WapPay internal/logic/alipay/wap_pay.go:18-22
同上 Alipay.PagePay internal/logic/alipay/page_pay.go:18-22
同上 Alipay.AppPay internal/logic/alipay/app_pay.go:18-22
同上 Alipay.Transfer internal/logic/alipay/transfer.go:18-22
有实现但业务逻辑未落地 Wechat.WxCallback(验签+解密后原样返回) internal/logic/wechat/wx_callback.go:38 注释 // Todo: 业务逻辑
有实现但入账函数无人调用 充值入账 models.ChargeWallet 定义于 internal/models/query.go:135,全仓库唯一引用为该定义本身(无调用点)

上述 7 个占位 RPC 的 Go 写法是 func(...) (reply *XxxReply, err error) { ...; return },即返回 (nil, nil)。gRPC 侧会回一个空消息grpc-gateway 侧对 nil 响应做序列化时会失败(客户端表现为 500/空响应),不是"返回空对象"。

4. 数据模型与表

模型统一在 init() 中通过 database.AppendMigrate 注册自动迁移(internal/models/*.goinit())。公共字段来自 SDKgorm.Model = id/created_at/updated_at/deleted_attypes.Std_Identity = identity varchar(36) uniqueIndextypes.Std_Passport = passport_idpassport_identity varchar(36) indextypes.Std_Status = status int64 default 0 index

wallet_basicinternal/models/wallet_basic.go:14-24

字段 类型 键/约束 说明
id uint PK 自增主键
created_at/updated_at/deleted_at timestamp index(deleted_at) 软删除
identity varchar(36) uniqueIndex 钱包唯一码
passport_id uint index 用户 ID
passport_identity varchar(36) index非唯一 用户唯一标识
alipay_id varchar(64) 默认 '' 绑定的支付宝 ID
wxpay_id varchar(64) 默认 '' 绑定的微信支付 ID
pay_password varchar(255) 默认 '' 支付密码HMAC-SHA256 十六进制串)
balance int64 默认 0 余额,单位:分
withdrawal_balance int64 默认 0 可提现余额,单位:分
status int64 index, 默认 0 状态(代码中把 -1 视为禁用,logic/basic/wallet.go:31

passport_identity 没有唯一约束,而懒创建路径(internal/models/query.go:76-92)在记录不存在时新建钱包 → 同一用户可能持有多条钱包记录。

wallet_paymentinternal/models/wallet_payment.go:16-31

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 支付单唯一码
passport_id / passport_identity uint / varchar(36) index 下单人
wallet_identity varchar(36) not null 钱包唯一码
order_no varchar(255) not null无唯一索引 订单号(充值单为随机串)
trade_no varchar(255) 默认 '' 第三方交易流水号;全仓库无写入点
type int8 默认 0 1 充值 / 2 电商订单
pay_channel int8 默认 0 1 微信 / 2 支付宝 / 3 钱包余额
pay_type varchar(255) not null JSAPI/APP/MINI/NATIVE/WAP 等
amount int64 默认 0 金额,单位:分
args varchar(255) 默认 '' 相关参数;无写入点
remark varchar(255) 默认 '' 备注;无写入点
callback_msg varchar(255) 默认 '' 回调信息
status int64 index, 默认 0 -3 手动取消 / -2 超时 / -1 失败 / 0 创建 / 1 支付中 / 2 成功

wallet_recordinternal/models/wallet_record.go:16-32

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 流水唯一码
passport_id / passport_identity uint / varchar(36) index 用户
wallet_identity varchar(36) not null 钱包唯一码
trans_type int8 index -1 支出 / 1 收入
in_trade_no varchar(50) not null无唯一索引 内部交易流水号
out_trade_no varchar(50) 默认 '' 外部交易流水号
money int64 unsigned 默认 0 金额,单位:分
fee int64 unsigned 默认 0 手续费,单位:分
pay_channel int8 默认 0 1 微信 / 2 支付宝 / 3 银行卡
pay_type varchar(255) not null 支付类型
trade_type int8 默认 0 1 充值 / 2 提现 / 3 消费 / 4 退款 / 5 收益
ymd int32 index, not null 年月日(用于当日统计)
ym int32 index, not null 年月(用于当月统计)
remark varchar(255) 默认 '' 备注

wallet_apply_cashinternal/models/wallet_apply_cash.go:16-28

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 提现单唯一码
passport_id / passport_identity uint / varchar(36) index 申请人
wallet_identity varchar(36) not null 钱包唯一码
cash_no varchar(255) 默认 '' 内部交易流水号;无写入点logic/basic/apply_cash.go:39-47 未设置)
trade_no varchar(255) 默认 '' 第三方流水号;无写入点
remark varchar(255) 默认 '' 备注
amount int64 默认 0 提现金额,单位:分
channel int8 默认 0 1 微信 / 2 支付宝 / 3 银行卡
status int8 默认 0 -1 失败 / 0 待处理 / 1 成功
call_back_msg varchar(255) 默认 '' 提现回调信息

wallet_bankinternal/models/wallet_bank.go:16-29

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 银行卡记录唯一码
passport_id / passport_identity uint / varchar(36) index 归属人
wallet_identity varchar(36) not null 钱包唯一码
card_no varchar(255) not null明文、无唯一约束 银行卡号
bank_name / bank varchar(255)/(64) not null 银行名 / 所属行
card_owner varchar(255) not null 持卡人姓名
id_card varchar(255) not null明文 持卡人身份证号
phone varchar(20) not null 银行预留手机号
bind_id varchar(20) 默认 '' 绑卡返回的 bind_id无写入点
bank_type varchar(20) 默认 '' DC 储蓄卡 / CC 信用卡

wallet_refundinternal/models/wallet_refund.go:16-25

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 退款单唯一码
passport_id / passport_identity uint / varchar(36) index 用户
wallet_identity varchar(36) not null 钱包唯一码
order_identity varchar(64) not null 订单唯一码
reason / order_info / result text 默认 '' 退款理由 / 订单信息 / 处理结果(均无写入点)

该表只在迁移注册中被引用,没有任何业务代码使用(退款链路未实现)。

直接访问的外部表

位置 用途
order_summary internal/models/query.go:104 order_no 读取 trans_price 作为订单支付金额
passport_account internal/models/query.go:24 InitData 中按硬编码 account='demo' 查默认用户

5. 核心流程

5.1 创建支付单 → 渠道下单 → 回调 → 流水

flowchart TD
    A["POST /wallet.Payment/ByCharge 或 ByOrder<br/>service.ParseMetaCtx JWT"] --> B["GetWalletByPassportIdentity<br/>不存在则懒创建钱包"]
    B --> C{"in.PayChannel"}
    C -->|"1 微信"| D["wechat.NewWechat 读私钥文件<br/>GetResponse 统一下单"]
    C -->|"2 支付宝"| E["alipay.NewAlipay<br/>SetBody -> TradeWapPay 或 TradeAppPay"]
    C -->|"3 钱包余额"| F["basic.NewWallet<br/>校验状态 支付密码 余额"]
    F --> G["TradeConsum 读到的余额减去金额后回写"]
    D --> H["models.CreatePaymentRecord<br/>写 wallet_payment status=0"]
    E --> H
    G --> H
    H --> I["渠道异步通知"]
    I --> J["wallet.Wechat/WxCallback<br/>VerifySignByPKMap 验签 + 解密"]
    J --> K["业务逻辑 TODO 原样返回 SUCCESS"]
    I --> L["wallet.Payment/Callback<br/>用户 JWT 直接指定成功或失败"]
    L --> M["UpsetWalletPaymentByIdentity<br/>只更新 status 与 callback_msg"]
    K --> N["不落库 不改状态"]
    M --> O["入账缺失"]
    O --> P["ChargeWallet 无调用点<br/>wallet_basic.balance 不增加"]
    P --> Q["wallet_record 不写流水"]

5.2 提现申请

flowchart TD
    A["POST /wallet.Basic/ApplyCash<br/>service.ParseMetaCtx JWT"] --> B["校验 amount 与 channel 非 0"]
    B --> C["GetWalletByPassportIdentity 读取余额"]
    C --> D{"in.Amount 大于 myWallet.Balance"}
    D -->|是| E["返回 ErrBalanceNotEnough"]
    D -->|否| F["Create wallet_apply_cash status=0"]
    F --> G["返回 OK"]
    G -.-> H["不扣减 balance 不冻结 不出款"]
    H -.-> I["不写 wallet_record 流水"]
    I -.-> J["无幂等键 cash_no 未写<br/>重复提交按次数叠加提现单"]

6. 审计发现

6.1 安全

级别 位置 问题
internal/logic/payment/callback.go:16-31 支付结果回调由客户端自证。接口只做 JWT 解析,随后用调用方给的 in.CallbackStatus 直接决定 status = 2(成功)或 -1(失败),只用 auth.Identity 作为 passport_identity 过滤条件定位支付单。没有渠道验签、没有金额核对、没有 trade_no 校验、没有状态机约束、没有幂等。任何持有有效 JWT 且知道(或猜到)自己支付单 identity 的调用方,都能把自己的待支付单标记为"支付成功"。
etc/wallet_test.yaml:33 + internal/logic/payment/callback.go:16 wallet.payment.Callback 被列入匿名清单(etc/wallet_dev.yaml:40 同类),而实现要求 JWTmeta.go:21-29)。渠道服务端回调不可能携带用户 JWT因此该接口作为真实回调必定失败;若网关按匿名放行,则等于把"支付成功"开关完全开放。配置与实现互相矛盾,两条路都不可接受。
internal/logic/wechat/jsapi_pre_order.go:10-27 Wechat.JsapiPreOrder 完全没有鉴权(函数体内无 ParseMetaCtx),且 amount 由请求方直接给出并原样作为微信下单金额(internal/logic/wechat/wechat.go:77-101Set("total", amount))。任何人都可匿名发起任意金额的微信下单。
internal/logic/basic/set_pay_password.go:23-29 修改支付密码不校验旧密码、不校验密码强度与长度(只判 in.Password == ""),直接 Update("pay_password", ...)。持有任意有效 JWT 即可覆盖支付密码,进而配合 PayChannel=3 的钱包扣款(logic/payment/by_order.go:139-146)完成盗刷。
internal/logic/basic/wallet.go:67-69 支付密码使用单轮 HMAC-SHA256git.apinb.com/bsm-sdk/core/utils/crypto.go:19-25key 为 passportIdentity + config.Spec.Wallet.PublicKey,结果以十六进制串明文存库(wallet_basic.pay_password。无迭代、无随机盐、无失败次数限制6 位数字密码(示例 test/basic.http:22123456)离线穷举仅 10^6 次 HMAC。
etc/wallet_prod.yaml:56etc/wallet_dev.yaml:73 微信支付凭据明文硬编码并提交入库APIV3Key: MakeSupplyChainFinance1123581321MchID: 1670561983SerialNo: 209F629E63E56D2366361969B51FBA0ADCF69F55AppID: wx9a2f163207219776。APIv3 密钥是微信支付回调报文解密密钥,泄露即可解密全部交易通知。
internal/logic/basic/apply_cash.go:19-53 提现申请不要求支付密码,也不校验提现渠道是否已绑定对应账户(Channel 只判非 0任意 JWT 即可为任意渠道生成提现单。
internal/logic/basic/bind_payment_id.go:23-36 PayType 只判 == 0,传 3 等非法值时 filed 保持空串,最终执行 Update("", in.AuthCode)(目标列名非法);同时把前端传入的 auth_code 直接当作 openid 落库,没有走 code2session 换取并校验 openid可绑定他人账号标识。
internal/logic/basic/add_bank_card.go:34-47internal/logic/basic/get_bank_card.go:37-47 银行卡号与身份证号明文存储、并以 BankNumber/IdCard 原样回显;add_bank_card.go:36Where("card_no = ?", in.CardNo) 未限定 passport,用于给任意卡号填充 bank_name/bank/bank_type,可据此探测某卡号是否已被其他用户绑定(存在性侧信道)。
internal/logic/wechat/wechat.go:68internal/logic/alipay/alipay.go:27 生产代码中开启 gopay 调试开关 client.DebugSwitch = gopay.DebugOn,会把渠道请求/响应含金额、out_trade_no、openid写入进程日志。

6.2 正确性与逻辑缺陷

级别 位置 问题
internal/logic/payment/by_order.go:37-49internal/logic/payment/by_charge.go:30-44 空指针 panicmyWallet.IdentitywErr 判空之前被求值(付款记录结构体字面量里直接引用),而 GetWalletByPassportIdentity 在非 ErrRecordNotFound 的错误上返回 (nil, err)internal/models/query.go:88)→ 查询一失败就在构造结构体时崩溃。
internal/logic/basic/wallet.go:53-63 余额扣减为读后写,且无事务/无行锁/无余额条件"balance": srv.Body.Balance - amount 用的是 NewWallet 时刻读到的旧值UPDATE 语句只有 WHERE identity = ?wallet.go:58)。并发两笔消费都能通过 wallet.go:42 的余额校验,最终写入同一个余额值 → 双花/丢更新。withdrawal_balance 同样被硬减,可能变为负数。
internal/logic/basic/apply_cash.go:35-49 提现只校验不扣减。校验 in.Amount > myWallet.Balance 后仅插入一条 wallet_apply_cash,既不扣 balance 也不冻结,且 cash_no 留空、无幂等键、无重复提交防护。余额 100 元可连续提交 N 张 100 元提现单。
internal/models/query.go:135-165 ChargeWallet唯一的钱包入账函数,但全仓库无调用点(仅有定义)。其内部还叠加三个缺陷:余额加法为读后写(query.go:143,与 6.2 第 2 条同型);余额更新与流水写入不在同一事务(query.go:146 / query.go:161CreateTradeRecord 的返回值被直接丢弃(query.go:161)。
全链路 充值/支付回调不会改变余额(对应 6.3)。ByCharge 只写 wallet_paymentby_charge.go:129WxCallback 不落库(wx_callback.go:38Payment.Callback 只改 status/callback_msginternal/models/query.go:119-122ChargeWallet 无人调用 → wallet_basic.balance 永不增加。
internal/logic/payment/by_charge.go:26-28 金额只判 in.Amount == 0负数金额可以通过(如 -100也没有最小/最大金额限制。
internal/logic/payment/callback.go:25-31 无状态机约束:任意状态可直接置为 2-1,无时间窗、无幂等(重复回调只是覆盖 callback_msgwallet_payment.trade_no 字段全仓库无写入点,因此回调后也拿不到渠道流水号,无法对账。
internal/models/query.go:168-197 FindWalletRecords 中 4 处 tx.Where(...) 未回写(query.go:174177182185),时间范围与收支/交易类型过滤实际不生效;返回的 total 用的是 res.RowsAffectedquery.go:196),即当页条数而非总条数,却被 logic/basic/transactions.go:33 直接作为 Total 返回。
internal/logic/basic/transactions.go:21-26 if in.PageSize <= 1 { in.PageSize = 20 }page_size = 1 被强制放大为 20page_size 无上限,配合 query.go:189Limit 可一次拉取全表流水。
internal/logic/basic/get_wallet.go:36-73 六个统计查询共用同一个 totalMoney 变量,每次 Scan 前不清零,且全部忽略 .Error。任一次 sum() 返回 NULL 或查询失败时,会把上一次的统计结果当作本次结果返回(例如"当日支出"显示成"当日收入"的金额)。
internal/logic/basic/get_wallet.go:41,47,53,59 当日/当月统计按 ymd/ym 过滤,但唯一的流水写入点(internal/models/query.go:151-158从不写 Ymd/Ym(落库为 0→ 当日/当月收支统计恒为 0。
internal/logic/alipay/alipay.go:46-49 + alipay.go:52-60 必然 panicNewAlipay 返回的 AliPay.Body 为 nil构造时只赋 Client),而 SetBody 第一句就是 srv.Body.Set(...)alipay.go:53。go-pay v1.5.123 的 BodyMap.Set 是值接收者(gopay/body_map.go:32),经 nil 指针调用会解引用空指针;已在同版本 gopay 上最小复现(panic: runtime error: invalid memory address or nil pointer dereference)。ByOrder/ByChargePayChannel == 2 分支必然触发(by_order.go:118by_charge.go:102)。
internal/logic/alipay/alipay.go:23-24,48 ctx 字段被注释掉、从未赋值TradeWapPay/TradeAppPay 拿到 nil contextalipay.go:63alipay.go:68)→ 即便修好 Body,渠道调用仍会以 nil Context 失败。
internal/logic/alipay/alipay.go:55-57 无论 WAP 还是 APP 都写死 product_code = QUICK_WAP_WAYAPP 应为 QUICK_MSECURITY_PAYtotal_amountamount/100 整数除法非整元金额被截断10050 分 → 100 元),且未按支付宝要求传两位小数字符串。
internal/logic/wechat/jsapi_pre_order.go:22internal/logic/wechat/wx_callback.go:26 wx, _ := NewWechat() 丢弃错误。NewWechat 在读不到私钥文件时返回 (nil, err)wechat.go:48-51),下一行访问 wx.Client 即 panic。
internal/logic/wechat/wechat.go:77-102146-173internal/logic/payment/by_order.go:95 GetResponseNativePaySign 的语义与用途不符:GetResponse 内部固定调用 V3TransactionJsapiwechat.go:96NativePaySign 实际调用 V3TransactionNativewechat.go:164)却接收一个 expire 参数;by_order.go:95 调用时该位置传空串,导致 Set("time_expire", "") 被发给微信。
internal/logic/payment/by_order.go:152-155by_charge.go:129-132 先向渠道下单、再写本地支付记录。写库失败时渠道侧订单已生成(孤儿单),且无幂等键,重复调用会为同一 order_no 生成多条 wallet_paymentorder_no 无唯一索引)。
internal/models/query.go:76-92 钱包懒创建:impl.DBService.Create(wallet) 的错误被忽略(query.go:85),失败时仍返回一个 Identity 有值但库中不存在的钱包,后续所有 UpdateColumns 静默影响 0 行;且 passport_identity 无唯一约束,并发下可为同一用户创建多条钱包,余额分裂。
internal/logic/basic/bind_payment_id.go:27-36 变量名 filed(应为 field拼写错误PayType 不在 {1,2} 时目标列为空串(见 6.1)。
internal/models/query.go:102-108 GetTransPriceByOrderNo 忽略 Scan.Error,以 amount == 0 代替错误判断,"0 元订单"与"订单不存在"无法区分;同时直接读 order_summary(跨模块直连表)。
internal/models/query.go:119 函数名 UpsetWalletPaymentByIdentity 拼写错误(应为 Upsert且实现是 Updates,语义为"更新"而非"更新或插入"。

6.3 未完成实现

类别 内容 证据
TODO 占位 RPC7 个) Wechat.AppPreOrderWechat.NativePreOrderWechat.TransferAlipay.WapPayAlipay.PagePayAlipay.AppPayAlipay.Transfer internal/logic/wechat/{app_pre_order,native_pre_order,transfer}.go:18-22internal/logic/alipay/{wap_pay,page_pay,app_pay,transfer}.go:18-22
回调业务逻辑未落地 WxCallback 验签+解密后 // Todo: 业务逻辑,直接返回 SUCCESS;不入库、不改支付状态、不入账 internal/logic/wechat/wx_callback.go:38-40
入账链路断裂 models.ChargeWallet(唯一加余额的函数)无任何调用点 定义 internal/models/query.go:135;全仓库引用仅此一处
退款链路未实现 WalletRefund 模型只注册迁移,无任何业务代码引用 internal/models/wallet_refund.go:27-29
启动依赖 Demo 数据 InitData 按硬编码 account='demo'passport_account,查不到即返回 errorsrv.Use 会 panicSDK service/service.go:133-138)→ 生产库中无 demo 账号时独立入口启动失败 internal/models/query.go:24-30cmd/main/main.go:47
支付单不可回填渠道流水 wallet_payment.trade_nowallet_apply_cash.cash_nowallet_bank.bind_idwallet_payment.args/remark 均无写入点 internal/models/wallet_payment.go:22,27-28internal/models/wallet_apply_cash.go:21-22internal/models/wallet_bank.go:26
文档与实现不符 README 声称存在 MakefileDockerfileswagger/scripts/ 目录以及 go run cmd/main/main.go migrate 迁移子命令,实际目录中均不存在,main.go:53-55 也只有 Run() README.md:53,66,92-93,269

6.4 健壮性与可维护性

级别 位置 问题
etc/wallet_prod.yaml:1-4etc/wallet_test.yaml:1-4 配置文件使用 Name/ListenOn/Dsn,而 SrvConfig 只识别 Service/Port/Databasesinternal/config/config.go:16-18。SDK 在解析前先检查文件是否含子串 "Service:"git.apinb.com/bsm-sdk/core/conf/new.go:58-60),这两个文件都不含 → log.Fatalln("ERROR: Service Not Nil") 直接退出。三个配置中只有 wallet_dev.yaml 能被加载
etc/wallet_dev.yaml(无 Wallet:/Alipay: 段)+ internal/config/config.go:25-27 config.Spec.Walletconfig.Spec.Alipay 保持 nilinternal/logic/payment/way.go:22,29,36internal/logic/alipay/alipay.go:53,59 直接解引用 → 扣款方式列表与支付宝下单均 panic。
internal/impl/impl.go:24internal/config/config.go:83 with.Databases(config.Spec.Databases, nil),但 conf.NotNil 只校验 Service/Cache未校验 DatabasesDatabases 缺失时 SDK 直接 panic("No Database Source Found !")git.apinb.com/bsm-sdk/core/with/databases.go:13-15)。
internal/server/new.go:16-30cmd/main/main.go:39 Server.Mux 声明后从未赋值,独立入口把 nil 传给 GatewayMux;而 grpc-gateway 的 RegisterXxxHandlerServer 只在 service/expose.go:30-41 调用(仅聚合入口触发)→ 独立进程启动 HTTP 网关时 handler 为 nil落到 DefaultServeMux),所有 REST 路径 404。
internal/impl/impl.go:22-23,25service/dependencies.go:29-31 RedisServiceMemorySerice 被初始化/注入,但全模块无任何使用点(死代码);EtcdService 仅在微服务注册时被 SDK 使用。
internal/logic/wechat/wechat.go:21-29,41-74 mchIDSerialNumbermchAPIv3KeyApiClientKeyAppId 等为包级可变变量,每次 NewWechat 都被重写且无同步;并发下单时存在数据竞争(go test -race 会报)。
internal/excode/ex.go:10-16 定义的 ErrPassportId/ErrAuthCode/ErrOrderNo/ErrAmount/ErrIdentityArgument/ErrPasswordArgument 中,ErrVerifySign(签名验证失败)全模块无使用点——与"回调不验签"的实际实现一致。
internal/logic/basic/get_wallet.go:74 残留注释 // remove useless fmt.Sprint side-effect-free call
全模块 0 个 *_test.gotest/ 下仅有 5 个 .http 手测样例与一行说明的 readme.md
README.md:444-491 README 中的建表 DDL 与真实模型不一致:wallet_payment DDL 缺 wallet_identity/trade_no/type/pay_channel/args/remarkwallet_record DDL 缺 fee/pay_type/ymd/ym/remark/passport_id

7. 风险汇总

编号 级别 问题 影响面
W1 Payment.Callback 凭用户 JWT 即可把支付单置为成功(无验签/无金额核对/无幂等) 资金安全、订单履约
W2 支付回调匿名清单与 JWT 实现矛盾,真实渠道回调不可用 支付链路可用性
W3 充值入账链路断裂:ChargeWallet 无调用点,回调不增加余额 核心功能不可用
W4 钱包余额扣减为读后写、无事务/条件更新 → 并发双花 资金安全
W5 提现只校验不扣减、无幂等键 → 重复提交超额提现 资金安全
W6 支付密码可被任意 JWT 直接覆盖;密码仅单轮 HMAC-SHA256 账户与资金安全
W7 支付宝分支 Body 为 nil → PayChannel=2 必然 panic 接口可用性
W8 ByOrder/ByChargewErr 判空前解引用 myWallet.Identity → 空指针 panic 接口可用性
W9 JsapiPreOrder 无鉴权且金额由请求方指定 资金安全、接口滥用
W10 微信 APIv3 密钥、商户号明文硬编码入库 渠道凭据泄露
W11 余额/提现/消费全链路不写 wallet_record 流水 对账与可追溯性
W12 生产与测试配置字段名不匹配,服务无法启动(独立入口) 部署可用性
W13 wallet_dev.yamlWallet/Alipay 段 → Way 等方法 nil 解引用 运行期崩溃
W14 Databases 未做非空校验,缺失即 panic 启动崩溃
W15 交易流水查询过滤失效、Total 语义错误、分页无上限 功能不正确 + 全表拉取
W16 GetWallet 六个统计共用变量且忽略错误;ymd/ym 从未写入 → 统计结果错误或恒为 0 数据展示错误
W17 7 个 RPC 为 TODO 占位,返回 (nil, nil) 功能缺失、网关异常
W18 银行卡号/身份证明文存储与回显;按卡号跨用户探测 隐私与合规
W19 独立入口 HTTP 网关 Mux 为 nilhandler 从未注册) 接口不可用
W20 渠道 client 包级变量并发写、NewWechat 错误被丢弃 并发崩溃
W21 死代码Redis/Memory 未用、ErrVerifySign 未用、无测试、README 与实现不符 可维护性

8. 修复建议(务实项)

  1. 回调改为渠道验签入口W1/W2/W3Payment.Callback 不应接受客户端提交的 CallbackStatus。把 status 的来源改为渠道回调(微信走 WxCallback 的验签+解密结果,支付宝走其异步通知验签),并在回调内完成:按 out_trade_no 反查支付单 → 用渠道返回的金额与 wallet_payment.amount 比对 → 校验当前 status 后置为 2(幂等:WHERE status IN (0,1))→ 在同一事务内调用 ChargeWallet 入账并写 wallet_record(补上 Ymd/Ym)。Payment.Callback 至多保留为内部/管理端接口,并从匿名清单中移除。
  2. 钱包扣减改成条件更新W4/W5TradeConsum 用一条语句完成 UPDATE wallet_basic SET balance = balance - ?, withdrawal_balance = withdrawal_balance - ? WHERE identity = ? AND balance >= ? AND status <> -1,按 RowsAffected == 0 判定余额不足;ApplyCash 在同一事务内先扣减/冻结余额再插提现单,并给 wallet_apply_cash 加上请求幂等键(写 cash_no,加唯一索引)。
  3. 支付密码W6SetPayPassword 增加旧密码校验分支(首次设置与修改分开);把 EncodePassword 换成 SDK 或 golang.org/x/crypto/bcrypt 的慢哈希,并在钱包表增加失败次数/锁定字段。密码长度做最小校验(当前示例数据用的 123456 应被拒)。
  4. 补齐 wallet_record 流水W11TradeConsum(消费)、ApplyCash(提现)、ChargeWallet(充值)三条路径都必须写流水,并把流水写入与余额变更放进同一事务;TradeConsum 目前只改余额不写流水。
  5. 修 panic 点W7/W8/W13models.WalletPayment{...} 的构造移到 wErr 判断之后;NewAlipay 初始化 BodyBody: &gopay.BodyMap{})并给 ctxcontext.Background();对 config.Spec.Wallet/Alipay/WeChatconfig.New 中做非空校验(与 conf.NotNil 一致的处理),或在 logic 入口显式判空返回错误。
  6. 微信下单W9/W20JsapiPreOrderservice.ParseMetaCtx;金额改由服务端按业务单据决定,不接受请求方直接指定;NewWechat 的返回错误不得丢弃;把渠道密钥从包级变量改为 WeChatPay 结构体字段。
  7. 配置W12/W14wallet_prod.yamlwallet_test.yamlSrvConfig 的字段名重写(Service/Port/Databases/Cache),补上 WalletAlipayMicroServiceGateway 段;config.New 里补 conf.NotNil(Spec.Databases);把 APIV3Key/MchID/PrivateKey 改为从环境变量注入,并立即轮换已泄露的 APIv3 密钥。
  8. 查询与统计W15/W16FindWalletRecords 的 4 处 tx.Where 回写 txtotal 用独立的 Count 查询;transactionspage_size 设上限;GetWallet 的 6 个统计各自使用独立变量并在 Scan 前清零、检查 .ErrorCreateTradeRecord 补写 Ymd/Ym
  9. 敏感数据W18wallet_bank.card_no/id_card 改为加密存储(或至少仅存掩码 + 密文),接口回显掩码;add_bank_card.go:36 的卡号查询加 passport_identity 条件,去掉存在性侧信道。
  10. 占位与死代码W17/W19/W217 个 TODO RPC 在实现前应显式返回 Unimplemented 错误而不是 nil, nil(避免网关侧空响应);internal/server/new.go 中给 MuxgwRuntime.NewServeMux() 并在独立入口注册 handler或直接在独立入口改走 service.Expose;删除 internal/impl 中未使用的 Redis/Memory 初始化与 ErrVerifySign(若已按第 1 条接入验签则保留)。
  11. 测试:至少为 TradeConsum(并发扣减)、ApplyCash(重复提交)、Callback(金额不符/重复回调)、FindWalletRecords(过滤与分页)各写一条正例 + 一条边界用例;用 go test -race 覆盖渠道 client 的并发使用。

本报告只列出与现有实现直接相关的修复项。为 wallet 引入"统一支付网关抽象""回调事件总线""资金账户 DDD 分层"一类改造不在建议范围内——上述 11 条都是对现有函数的最小修正。

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

编号 级别 问题 处理结果
W1 Payment.Callback 凭用户 JWT 即可把支付单置为成功(无验签/无金额核对/无幂等) 已修复:回调改为渠道验签/解密后再取状态,并与本地支付单金额核对;增加幂等判断,重复回调不重复入账
W2 支付回调匿名清单与 JWT 实现矛盾,真实渠道回调不可用 已修复:回调鉴权口径与实现对齐,渠道回调路径可用
W3 充值入账链路断裂(ChargeWallet 无调用点,回调不增加余额) 已修复:回调成功后调用入账,余额与交易流水同步写入
W4 钱包余额扣减读后写、无事务/条件更新(并发双花) 已修复:改为条件更新 WHERE balance >= ? 并检查 RowsAffected,扣减失败即返回余额不足
W5 提现只校验不扣减、无幂等键 已修复:申请提现时在事务内真实扣减余额并写流水,加幂等防护防重复提交
W6 支付密码可被任意 JWT 直接覆盖;密码仅单轮 HMAC-SHA256 已修复:支付密码存储改为 bcryptDefaultCost)并加 6~32 位纯数字格式校验;已设置后不再允许任意登录态直接覆盖(现契约无「旧密码」字段,已存在 bcrypt 密码时返回 ErrPermissionDenied,需走安全重置流程)。完整「校验旧密码后改密」需要新增 proto 字段,属契约外,未做
W7 支付宝分支 Body 为 nil 导致 PayChannel=2 必然 panic 已修复:修正请求体初始化
W8 ByOrder/ByChargewErr 判空前解引用 myWallet.Identity 已修复:先判错再使用,避免空指针 panic
W9 JsapiPreOrder 无鉴权且金额由请求方指定 已修复:补鉴权,金额改为服务端按本地支付单核对
W10 微信 APIv3 密钥、商户号明文硬编码入库 已修复:凭据改为从配置/环境变量读取,不再明文写库
W11 余额/提现/消费全链路不写 wallet_record 流水 已修复:充值、消费、提现均写入交易流水
W12 生产与测试配置字段名不匹配,服务无法启动 已修复:按 SDK 规范重写 etc/wallet_prod.yamletc/wallet_test.yaml
W13 wallet_dev.yamlWallet/Alipay 段导致 nil 解引用 已修复:补齐配置段
W14 Databases 未做非空校验,缺失即 panic 已修复:internal/config 增加非空校验,缺失时给出明确中文提示

未纳入本轮范围

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

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