All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m36s
- 新增 tb_gateway_readonly_card(卡 ICCID / 设备 IMEI)与开关 JUNHONG_GATEWAY_READONLY_ENABLED, 替换环境变量名单 JUNHONG_GATEWAY_READONLY_ICCIDS - 只读资产只放行查卡状态、查实名、查流量、查设备信息、查设备槽位、同步设备状态; 停机、复机、限速、实名认证链接与设备写操作在网关出口本地拦截;查表失败按拦截;不缓存 - 测试环境部署开启只读模式;新增技术方案与清理顺序文档 Change: add-gateway-readonly-prod-import Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.9 KiB
Go
84 lines
2.9 KiB
Go
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
)
|
||
|
||
// readonlyAllowedPaths 是只读资产允许调用的查询类网关接口,这些调用不查只读表。
|
||
var readonlyAllowedPaths = map[string]struct{}{
|
||
"/flow-card/status": {},
|
||
"/flow-card/realName": {},
|
||
"/flow-card/flow": {},
|
||
"/device/info": {},
|
||
"/device/slot-info": {},
|
||
"/device/sync-info": {},
|
||
}
|
||
|
||
// ReadonlyChecker 判断网关请求卡号(卡为 ICCID,设备为 IMEI)是否为只读资产。
|
||
type ReadonlyChecker func(ctx context.Context, identifier string) (bool, error)
|
||
|
||
// WithReadonlyChecker 开启只读模式(支持链式调用):只读资产只允许查询类调用,
|
||
// 停机、复机、限速、实名认证链接与设备写操作在本地拦截、不发请求。checker 为 nil 时不生效(生产不开启)。
|
||
func (c *Client) WithReadonlyChecker(checker ReadonlyChecker) *Client {
|
||
c.readonlyChecker = checker
|
||
if checker != nil && c.logger != nil {
|
||
c.logger.Warn("Gateway 只读模式已开启:只读资产只允许查询类调用,其余调用一律拦截")
|
||
}
|
||
return c
|
||
}
|
||
|
||
// rejectReadonlyWrite 判断本次请求是否为只读资产的非查询调用;是则记日志并返回明确失败(不是结果未知),不发请求。
|
||
// 查只读表失败也按拦截处理:宁可调用失败,不放过真卡。
|
||
func (c *Client) rejectReadonlyWrite(ctx context.Context, path string, params interface{}) error {
|
||
if c.readonlyChecker == nil {
|
||
return nil
|
||
}
|
||
if _, ok := readonlyAllowedPaths[path]; ok {
|
||
return nil
|
||
}
|
||
cardNo := requestCardNo(params)
|
||
if cardNo == "" {
|
||
return nil
|
||
}
|
||
readonly, err := c.readonlyChecker(ctx, cardNo)
|
||
if err == nil && !readonly {
|
||
return nil
|
||
}
|
||
if c.logger != nil {
|
||
fields := []zap.Field{zap.String("path", path), zap.String("card_no", cardNo), zap.String("metric", "gateway.readonly_card.blocked")}
|
||
if err != nil {
|
||
fields = append(fields, zap.Error(err))
|
||
c.logger.Error("Gateway 只读表查询失败,按只读拦截,未发送请求", fields...)
|
||
} else {
|
||
c.logger.Warn("Gateway 只读资产非查询调用已拦截,未发送请求", fields...)
|
||
}
|
||
}
|
||
return newGatewayError(constants.GatewayErrorUpstream, errors.New(errors.CodeGatewayError, "测试环境只读资产,已拦截调用:"+path))
|
||
}
|
||
|
||
// requestCardNo 取请求里的卡号(cardNo 或 iccid 字段,大写),取不到时返回空串。
|
||
func requestCardNo(params interface{}) string {
|
||
raw, err := json.Marshal(params)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
var fields struct {
|
||
CardNo string `json:"cardNo"`
|
||
ICCID string `json:"iccid"`
|
||
}
|
||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||
return ""
|
||
}
|
||
if fields.CardNo != "" {
|
||
return strings.ToUpper(strings.TrimSpace(fields.CardNo))
|
||
}
|
||
return strings.ToUpper(strings.TrimSpace(fields.ICCID))
|
||
}
|