diff --git a/app/main/api/desc/admin/admin_query_whitelist.api b/app/main/api/desc/admin/admin_query_whitelist.api new file mode 100644 index 0000000..2caa83c --- /dev/null +++ b/app/main/api/desc/admin/admin_query_whitelist.api @@ -0,0 +1,91 @@ +syntax = "v1" + +info ( + title: "后台查询白名单管理服务" + desc: "对接天远 API 查询白名单配置接口" + version: "v1" +) + +@server ( + prefix: /api/v1/admin/query-whitelist + group: admin_query_whitelist + middleware: AdminAuthInterceptor +) +service main { + // 创建查询白名单规则 + @handler AdminCreateQueryWhitelist + post /create (AdminCreateQueryWhitelistReq) returns (AdminCreateQueryWhitelistResp) + + // 追加产品编码到已有规则 + @handler AdminAppendQueryWhitelist + // 获取操作记录列表 + @handler AdminGetQueryWhitelistOpLogList + get /op-log/list (AdminGetQueryWhitelistOpLogListReq) returns (AdminGetQueryWhitelistOpLogListResp) +} + +type ( + AdminQueryWhitelistEntry { + Id string `json:"id"` + Name string `json:"name"` + IdCardMasked string `json:"id_card_masked"` + ApiCodes []string `json:"api_codes"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } + AdminCreateQueryWhitelistReq { + Name string `json:"name,optional"` + IdCard string `json:"id_card"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark,optional"` + } + AdminCreateQueryWhitelistResp { + Code int `json:"code"` + Message string `json:"message"` + TransactionId string `json:"transaction_id"` + Entry *AdminQueryWhitelistEntry `json:"entry,optional"` + } + AdminAppendQueryWhitelistReq { + Name string `json:"name,optional"` + IdCard string `json:"id_card"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark,optional"` + } + AdminAppendQueryWhitelistResp { + Code int `json:"code"` + Message string `json:"message"` + TransactionId string `json:"transaction_id"` + Entry *AdminQueryWhitelistEntry `json:"entry,optional"` + } + AdminGetQueryWhitelistOpLogListReq { + Page int64 `form:"page"` + PageSize int64 `form:"pageSize"` + IdCard *string `form:"id_card,optional"` + Action *string `form:"action,optional"` + TianyuanCode *int64 `form:"tianyuan_code,optional"` + } + AdminQueryWhitelistOpLogItem { + Id string `json:"id"` + AdminUserId string `json:"admin_user_id"` + AdminUsername string `json:"admin_username"` + Action string `json:"action"` + ActionText string `json:"action_text"` + Name string `json:"name"` + IdCard string `json:"id_card"` + IdCardMasked string `json:"id_card_masked"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark"` + TianyuanCode int64 `json:"tianyuan_code"` + TianyuanMessage string `json:"tianyuan_message"` + TransactionId string `json:"transaction_id"` + EntryId string `json:"entry_id"` + EntryStatus string `json:"entry_status"` + EntryApiCodes []string `json:"entry_api_codes"` + CreateTime string `json:"create_time"` + } + AdminGetQueryWhitelistOpLogListResp { + Total int64 `json:"total"` + Items []AdminQueryWhitelistOpLogItem `json:"items"` + } +) diff --git a/app/main/api/desc/main.api b/app/main/api/desc/main.api index e349c43..42e5d1b 100644 --- a/app/main/api/desc/main.api +++ b/app/main/api/desc/main.api @@ -25,6 +25,7 @@ import "./admin/notification.api" import "./admin/admin_product.api" import "./admin/admin_feature.api" import "./admin/admin_whitelist.api" +import "./admin/admin_query_whitelist.api" import "./admin/admin_query.api" import "./admin/admin_agent.api" import "./admin/admin_api.api" diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index bb4812e..2d82013 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -94,6 +94,7 @@ Tianyuanapi: Key: "04c6b4c559be6d5ba5351c04c8713a64" BaseURL: "https://api.tianyuanapi.com" Timeout: 60 + WhitelistMgmtKey: "2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL" # 查询白名单管理密钥,向平台商务/技术支持申请 Authorization: FileBaseURL: "https://www.onecha.cn/api/v1/auth-docs" # 授权书文件访问基础URL Promotion: diff --git a/app/main/api/etc/main.yaml b/app/main/api/etc/main.yaml index 2c6f164..b25fae3 100644 --- a/app/main/api/etc/main.yaml +++ b/app/main/api/etc/main.yaml @@ -84,6 +84,7 @@ Tianyuanapi: Key: "04c6b4c559be6d5ba5351c04c8713a64" BaseURL: "https://api.tianyuanapi.com" Timeout: 60 + WhitelistMgmtKey: "2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL" # 查询白名单管理密钥,向平台商务/技术支持申请 Authorization: FileBaseURL: "https://www.onecha.cn/api/v1/auth-docs" # 授权书文件访问基础URL Promotion: diff --git a/app/main/api/internal/config/config.go b/app/main/api/internal/config/config.go index ece26e6..477f273 100644 --- a/app/main/api/internal/config/config.go +++ b/app/main/api/internal/config/config.go @@ -102,10 +102,11 @@ type TaxConfig struct { TaxExemptionAmount float64 } type TianyuanapiConfig struct { - AccessID string - Key string - BaseURL string - Timeout int64 + AccessID string + Key string + BaseURL string + Timeout int64 + WhitelistMgmtKey string // 查询白名单管理密钥 Whitelist-Mgmt-Key } type AuthorizationConfig struct { diff --git a/app/main/api/internal/handler/admin_query_whitelist/adminappendquerywhitelisthandler.go b/app/main/api/internal/handler/admin_query_whitelist/adminappendquerywhitelisthandler.go new file mode 100644 index 0000000..c160a82 --- /dev/null +++ b/app/main/api/internal/handler/admin_query_whitelist/adminappendquerywhitelisthandler.go @@ -0,0 +1,29 @@ +package admin_query_whitelist + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/admin_query_whitelist" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" +) + +func AdminAppendQueryWhitelistHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminAppendQueryWhitelistReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query_whitelist.NewAdminAppendQueryWhitelistLogic(r.Context(), svcCtx) + resp, err := l.AdminAppendQueryWhitelist(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query_whitelist/admincreatequerywhitelisthandler.go b/app/main/api/internal/handler/admin_query_whitelist/admincreatequerywhitelisthandler.go new file mode 100644 index 0000000..34c8044 --- /dev/null +++ b/app/main/api/internal/handler/admin_query_whitelist/admincreatequerywhitelisthandler.go @@ -0,0 +1,29 @@ +package admin_query_whitelist + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/admin_query_whitelist" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" +) + +func AdminCreateQueryWhitelistHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateQueryWhitelistReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query_whitelist.NewAdminCreateQueryWhitelistLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateQueryWhitelist(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query_whitelist/admingetquerywhitelistoploglisthandler.go b/app/main/api/internal/handler/admin_query_whitelist/admingetquerywhitelistoploglisthandler.go new file mode 100644 index 0000000..a7c521f --- /dev/null +++ b/app/main/api/internal/handler/admin_query_whitelist/admingetquerywhitelistoploglisthandler.go @@ -0,0 +1,29 @@ +package admin_query_whitelist + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/admin_query_whitelist" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" +) + +func AdminGetQueryWhitelistOpLogListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetQueryWhitelistOpLogListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query_whitelist.NewAdminGetQueryWhitelistOpLogListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetQueryWhitelistOpLogList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 9c5ceff..a71f064 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -16,6 +16,7 @@ import ( admin_platform_user "ycc-server/app/main/api/internal/handler/admin_platform_user" admin_product "ycc-server/app/main/api/internal/handler/admin_product" admin_query "ycc-server/app/main/api/internal/handler/admin_query" + admin_query_whitelist "ycc-server/app/main/api/internal/handler/admin_query_whitelist" admin_role "ycc-server/app/main/api/internal/handler/admin_role" admin_role_api "ycc-server/app/main/api/internal/handler/admin_role_api" admin_user "ycc-server/app/main/api/internal/handler/admin_user" @@ -688,6 +689,30 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1/admin/whitelist"), ) + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AdminAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodGet, + Path: "/op-log/list", + Handler: admin_query_whitelist.AdminGetQueryWhitelistOpLogListHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/append", + Handler: admin_query_whitelist.AdminAppendQueryWhitelistHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/create", + Handler: admin_query_whitelist.AdminCreateQueryWhitelistHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1/admin/query-whitelist"), + ) + server.AddRoutes( []rest.Route{ { diff --git a/app/main/api/internal/logic/admin_query_whitelist/adminappendquerywhitelistlogic.go b/app/main/api/internal/logic/admin_query_whitelist/adminappendquerywhitelistlogic.go new file mode 100644 index 0000000..39cb0da --- /dev/null +++ b/app/main/api/internal/logic/admin_query_whitelist/adminappendquerywhitelistlogic.go @@ -0,0 +1,46 @@ +package admin_query_whitelist + +import ( + "context" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminAppendQueryWhitelistLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminAppendQueryWhitelistLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminAppendQueryWhitelistLogic { + return &AdminAppendQueryWhitelistLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminAppendQueryWhitelistLogic) AdminAppendQueryWhitelist(req *types.AdminAppendQueryWhitelistReq) (resp *types.AdminAppendQueryWhitelistResp, err error) { + if err = validateQueryWhitelistReq(req.IdCard, req.ApiCodes); err != nil { + return nil, err + } + + apiResp, err := callQueryWhitelistMgmt( + l.svcCtx, + "/api/v1/query-whitelist/entries/append", + req.Name, + req.IdCard, + req.ApiCodes, + req.Remark, + ) + if err != nil { + return nil, err + } + + recordQueryWhitelistOpLog(l.ctx, l.svcCtx, "append", req.Name, req.IdCard, req.ApiCodes, req.Remark, apiResp) + + return toAppendResp(apiResp), nil +} diff --git a/app/main/api/internal/logic/admin_query_whitelist/admincreatequerywhitelistlogic.go b/app/main/api/internal/logic/admin_query_whitelist/admincreatequerywhitelistlogic.go new file mode 100644 index 0000000..27fd4da --- /dev/null +++ b/app/main/api/internal/logic/admin_query_whitelist/admincreatequerywhitelistlogic.go @@ -0,0 +1,46 @@ +package admin_query_whitelist + +import ( + "context" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminCreateQueryWhitelistLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateQueryWhitelistLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateQueryWhitelistLogic { + return &AdminCreateQueryWhitelistLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateQueryWhitelistLogic) AdminCreateQueryWhitelist(req *types.AdminCreateQueryWhitelistReq) (resp *types.AdminCreateQueryWhitelistResp, err error) { + if err = validateQueryWhitelistReq(req.IdCard, req.ApiCodes); err != nil { + return nil, err + } + + apiResp, err := callQueryWhitelistMgmt( + l.svcCtx, + "/api/v1/query-whitelist/entries", + req.Name, + req.IdCard, + req.ApiCodes, + req.Remark, + ) + if err != nil { + return nil, err + } + + recordQueryWhitelistOpLog(l.ctx, l.svcCtx, "create", req.Name, req.IdCard, req.ApiCodes, req.Remark, apiResp) + + return toCreateResp(apiResp), nil +} diff --git a/app/main/api/internal/logic/admin_query_whitelist/admingetquerywhitelistoploglistlogic.go b/app/main/api/internal/logic/admin_query_whitelist/admingetquerywhitelistoploglistlogic.go new file mode 100644 index 0000000..367faf5 --- /dev/null +++ b/app/main/api/internal/logic/admin_query_whitelist/admingetquerywhitelistoploglistlogic.go @@ -0,0 +1,119 @@ +package admin_query_whitelist + +import ( + "context" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/app/main/model" + "ycc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetQueryWhitelistOpLogListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetQueryWhitelistOpLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetQueryWhitelistOpLogListLogic { + return &AdminGetQueryWhitelistOpLogListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetQueryWhitelistOpLogListLogic) AdminGetQueryWhitelistOpLogList(req *types.AdminGetQueryWhitelistOpLogListReq) (resp *types.AdminGetQueryWhitelistOpLogListResp, err error) { + builder := l.svcCtx.QueryWhitelistOpLogModel.SelectBuilder() + + if req.IdCard != nil && *req.IdCard != "" { + builder = builder.Where("id_card LIKE ?", "%"+*req.IdCard+"%") + } + if req.Action != nil && *req.Action != "" { + builder = builder.Where("action = ?", *req.Action) + } + if req.TianyuanCode != nil { + builder = builder.Where("tianyuan_code = ?", *req.TianyuanCode) + } + + total, err := l.svcCtx.QueryWhitelistOpLogModel.FindCount(l.ctx, builder, "id") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询操作记录总数失败, %v", err) + } + + list, err := l.svcCtx.QueryWhitelistOpLogModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询操作记录列表失败, %v", err) + } + + adminNameMap := l.buildAdminUsernameMap(list) + items := make([]types.AdminQueryWhitelistOpLogItem, 0, len(list)) + for _, row := range list { + items = append(items, types.AdminQueryWhitelistOpLogItem{ + Id: row.Id, + AdminUserId: row.AdminUserId, + AdminUsername: adminNameMap[row.AdminUserId], + Action: row.Action, + ActionText: queryWhitelistActionText(row.Action), + Name: row.Name, + IdCard: row.IdCard, + IdCardMasked: row.IdCardMasked.String, + ApiCodes: parseApiCodesJSON(row.ApiCodes), + Remark: row.Remark.String, + TianyuanCode: row.TianyuanCode, + TianyuanMessage: row.TianyuanMessage.String, + TransactionId: row.TransactionId.String, + EntryId: row.EntryId.String, + EntryStatus: row.EntryStatus.String, + EntryApiCodes: parseApiCodesJSON(row.EntryApiCodes.String), + CreateTime: row.CreateTime.Format("2006-01-02 15:04:05"), + }) + } + + return &types.AdminGetQueryWhitelistOpLogListResp{ + Total: total, + Items: items, + }, nil +} + +func (l *AdminGetQueryWhitelistOpLogListLogic) buildAdminUsernameMap(list []*model.QueryWhitelistOpLog) map[string]string { + nameMap := make(map[string]string) + if len(list) == 0 { + return nameMap + } + + adminIds := make([]string, 0, len(list)) + seen := make(map[string]struct{}, len(list)) + for _, row := range list { + if row.AdminUserId == "" { + continue + } + if _, ok := seen[row.AdminUserId]; ok { + continue + } + seen[row.AdminUserId] = struct{}{} + adminIds = append(adminIds, row.AdminUserId) + } + if len(adminIds) == 0 { + return nameMap + } + + builder := l.svcCtx.AdminUserModel.SelectBuilder().Where(squirrel.Eq{"id": adminIds}) + users, err := l.svcCtx.AdminUserModel.FindAll(l.ctx, builder, "") + if err != nil { + l.Errorf("批量查询管理员名称失败: %v", err) + return nameMap + } + for _, user := range users { + displayName := user.RealName + if displayName == "" { + displayName = user.Username + } + nameMap[user.Id] = displayName + } + return nameMap +} diff --git a/app/main/api/internal/logic/admin_query_whitelist/query_whitelist_helper.go b/app/main/api/internal/logic/admin_query_whitelist/query_whitelist_helper.go new file mode 100644 index 0000000..b381b1f --- /dev/null +++ b/app/main/api/internal/logic/admin_query_whitelist/query_whitelist_helper.go @@ -0,0 +1,199 @@ +package admin_query_whitelist + +import ( + "context" + "database/sql" + "encoding/json" + "regexp" + "strings" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + tianyuanapi "ycc-server/app/main/api/internal/service/tianyuanapi_sdk" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +var idCardPattern = regexp.MustCompile(`^\d{17}[\dXx]$`) + +func validateQueryWhitelistReq(idCard string, apiCodes []string) error { + if strings.TrimSpace(idCard) == "" { + return errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "") + } + if !idCardPattern.MatchString(strings.TrimSpace(idCard)) { + return errors.Wrapf(xerr.NewErrMsg("身份证号格式不正确,需为18位中国大陆身份证号"), "") + } + if len(apiCodes) == 0 { + return errors.Wrapf(xerr.NewErrMsg("请至少选择一个产品编码"), "") + } + for _, code := range apiCodes { + if strings.TrimSpace(code) == "" { + return errors.Wrapf(xerr.NewErrMsg("产品编码不能为空"), "") + } + if code == "*" { + return errors.Wrapf(xerr.NewErrMsg("产品编码不支持通配符 *"), "") + } + } + return nil +} + +func buildQueryWhitelistPayload(name, idCard string, apiCodes []string, remark string) map[string]interface{} { + payload := map[string]interface{}{ + "name": name, + "id_card": strings.TrimSpace(idCard), + "api_codes": apiCodes, + } + if strings.TrimSpace(remark) != "" { + payload["remark"] = strings.TrimSpace(remark) + } + return payload +} + +func callQueryWhitelistMgmt( + svcCtx *svc.ServiceContext, + apiPath string, + name, idCard string, + apiCodes []string, + remark string, +) (*tianyuanapi.QueryWhitelistMgmtResponse, error) { + mgmtKey := svcCtx.Config.Tianyuanapi.WhitelistMgmtKey + if mgmtKey == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("查询白名单管理密钥未配置,请在服务端配置 Tianyuanapi.WhitelistMgmtKey"), "") + } + if svcCtx.TianyuanapiClient == nil { + return nil, errors.Wrapf(xerr.NewErrMsg("天远 API 客户端未初始化"), "") + } + + if strings.TrimSpace(name) == "" { + name = "*" + } + + payload := buildQueryWhitelistPayload(name, idCard, apiCodes, remark) + return svcCtx.TianyuanapiClient.CallQueryWhitelistMgmt(apiPath, payload, mgmtKey) +} + +func toAdminEntry(entry *tianyuanapi.QueryWhitelistEntry) *types.AdminQueryWhitelistEntry { + if entry == nil { + return nil + } + return &types.AdminQueryWhitelistEntry{ + Id: entry.ID, + Name: entry.Name, + IdCardMasked: entry.IdCardMasked, + ApiCodes: entry.ApiCodes, + Status: entry.Status, + Remark: entry.Remark, + CreatedAt: entry.CreatedAt, + UpdatedAt: entry.UpdatedAt, + } +} + +func toCreateResp(resp *tianyuanapi.QueryWhitelistMgmtResponse) *types.AdminCreateQueryWhitelistResp { + return &types.AdminCreateQueryWhitelistResp{ + Code: resp.Code, + Message: resp.Message, + TransactionId: resp.TransactionID, + Entry: toAdminEntry(resp.Entry), + } +} + +func toAppendResp(resp *tianyuanapi.QueryWhitelistMgmtResponse) *types.AdminAppendQueryWhitelistResp { + return &types.AdminAppendQueryWhitelistResp{ + Code: resp.Code, + Message: resp.Message, + TransactionId: resp.TransactionID, + Entry: toAdminEntry(resp.Entry), + } +} + +func resolveName(name string) string { + if strings.TrimSpace(name) == "" { + return "*" + } + return strings.TrimSpace(name) +} + +func parseApiCodesJSON(raw string) []string { + if raw == "" { + return nil + } + var codes []string + if err := json.Unmarshal([]byte(raw), &codes); err != nil { + return nil + } + return codes +} + +func queryWhitelistActionText(action string) string { + switch action { + case "create": + return "创建规则" + case "append": + return "追加接口" + default: + return action + } +} + +func recordQueryWhitelistOpLog( + ctx context.Context, + svcCtx *svc.ServiceContext, + action, name, idCard string, + apiCodes []string, + remark string, + apiResp *tianyuanapi.QueryWhitelistMgmtResponse, +) { + adminUserId, err := ctxdata.GetUidFromCtx(ctx) + if err != nil { + logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: 获取管理员ID失败, %v", err) + adminUserId = "" + } + + apiCodesJSON, marshalErr := json.Marshal(apiCodes) + if marshalErr != nil { + logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: 序列化 api_codes 失败, %v", marshalErr) + return + } + + log := &model.QueryWhitelistOpLog{ + AdminUserId: adminUserId, + Action: action, + Name: resolveName(name), + IdCard: strings.TrimSpace(idCard), + ApiCodes: string(apiCodesJSON), + TianyuanCode: int64(apiResp.Code), + } + + if strings.TrimSpace(remark) != "" { + log.Remark = sql.NullString{String: strings.TrimSpace(remark), Valid: true} + } + if apiResp.Message != "" { + log.TianyuanMessage = sql.NullString{String: apiResp.Message, Valid: true} + } + if apiResp.TransactionID != "" { + log.TransactionId = sql.NullString{String: apiResp.TransactionID, Valid: true} + } + if apiResp.Entry != nil { + if apiResp.Entry.IdCardMasked != "" { + log.IdCardMasked = sql.NullString{String: apiResp.Entry.IdCardMasked, Valid: true} + } + if apiResp.Entry.ID != "" { + log.EntryId = sql.NullString{String: apiResp.Entry.ID, Valid: true} + } + if apiResp.Entry.Status != "" { + log.EntryStatus = sql.NullString{String: apiResp.Entry.Status, Valid: true} + } + if len(apiResp.Entry.ApiCodes) > 0 { + entryCodesJSON, _ := json.Marshal(apiResp.Entry.ApiCodes) + log.EntryApiCodes = sql.NullString{String: string(entryCodesJSON), Valid: true} + } + } + + if _, insertErr := svcCtx.QueryWhitelistOpLogModel.Insert(ctx, nil, log); insertErr != nil { + logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: %v", insertErr) + } +} diff --git a/app/main/api/internal/service/tianyuanapi_sdk/query_whitelist.go b/app/main/api/internal/service/tianyuanapi_sdk/query_whitelist.go new file mode 100644 index 0000000..c0079ae --- /dev/null +++ b/app/main/api/internal/service/tianyuanapi_sdk/query_whitelist.go @@ -0,0 +1,105 @@ +package tianyuanapi + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// QueryWhitelistEntry 查询白名单规则详情(解密后) +type QueryWhitelistEntry struct { + ID string `json:"id"` + Name string `json:"name"` + IdCardMasked string `json:"id_card_masked"` + ApiCodes []string `json:"api_codes"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// QueryWhitelistMgmtResponse 查询白名单管理接口响应 +type QueryWhitelistMgmtResponse struct { + Code int `json:"code"` + Message string `json:"message"` + TransactionID string `json:"transaction_id"` + Entry *QueryWhitelistEntry `json:"entry,omitempty"` + Timeout int64 `json:"timeout"` +} + +// CallQueryWhitelistMgmt 调用查询白名单管理接口(创建/追加) +func (c *Client) CallQueryWhitelistMgmt(apiPath string, params map[string]interface{}, mgmtKey string) (*QueryWhitelistMgmtResponse, error) { + startTime := time.Now() + + if mgmtKey == "" { + return nil, fmt.Errorf("Whitelist-Mgmt-Key 不能为空") + } + if params == nil { + return nil, fmt.Errorf("params不能为空") + } + + jsonData, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("参数序列化失败: %v", err) + } + + encryptedData, err := c.encrypt(string(jsonData)) + if err != nil { + return nil, fmt.Errorf("数据加密失败: %v", err) + } + + requestBody := map[string]string{"data": encryptedData} + requestBodyBytes, err := json.Marshal(requestBody) + if err != nil { + return nil, fmt.Errorf("请求体序列化失败: %v", err) + } + + url := c.baseURL + apiPath + httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestBodyBytes)) + if err != nil { + return nil, fmt.Errorf("创建HTTP请求失败: %v", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Access-Id", c.accessID) + httpReq.Header.Set("Whitelist-Mgmt-Key", mgmtKey) + httpReq.Header.Set("User-Agent", "TianyuanAPI-Go-SDK/1.0.0") + + resp, err := c.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %v", err) + } + + var apiResp ApiResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("响应解析失败: %v", err) + } + + result := &QueryWhitelistMgmtResponse{ + Code: apiResp.Code, + Message: apiResp.Message, + TransactionID: apiResp.TransactionID, + Timeout: time.Since(startTime).Milliseconds(), + } + + if apiResp.Data != "" { + decryptedData, decryptErr := c.decrypt(apiResp.Data) + if decryptErr == nil { + var entry QueryWhitelistEntry + if json.Unmarshal([]byte(decryptedData), &entry) == nil { + result.Entry = &entry + } + } + } + + return result, nil +} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go index f9151ec..79afee3 100644 --- a/app/main/api/internal/svc/servicecontext.go +++ b/app/main/api/internal/svc/servicecontext.go @@ -37,11 +37,13 @@ type ServiceContext struct { // 天元API调用记录模型 TianyuanapiCallLogModel model.TianyuanapiCallLogModel TianyuanapiCallLogService *service.TianyuanapiCallLogService + TianyuanapiClient *tianyuanapi.Client // 白名单相关模型 - UserFeatureWhitelistModel model.UserFeatureWhitelistModel - WhitelistOrderModel model.WhitelistOrderModel - WhitelistOrderItemModel model.WhitelistOrderItemModel + UserFeatureWhitelistModel model.UserFeatureWhitelistModel + WhitelistOrderModel model.WhitelistOrderModel + WhitelistOrderItemModel model.WhitelistOrderItemModel + QueryWhitelistOpLogModel model.QueryWhitelistOpLogModel // 订单相关模型 OrderModel model.OrderModel @@ -140,6 +142,7 @@ func NewServiceContext(c config.Config) *ServiceContext { userFeatureWhitelistModel := model.NewUserFeatureWhitelistModel(db, cacheConf) whitelistOrderModel := model.NewWhitelistOrderModel(db, cacheConf) whitelistOrderItemModel := model.NewWhitelistOrderItemModel(db, cacheConf) + queryWhitelistOpLogModel := model.NewQueryWhitelistOpLogModel(db, cacheConf) // ============================== 订单相关模型 ============================== orderModel := model.NewOrderModel(db, cacheConf) @@ -254,11 +257,13 @@ func NewServiceContext(c config.Config) *ServiceContext { // 天元API调用记录模型 TianyuanapiCallLogModel: tianyuanapiCallLogModel, TianyuanapiCallLogService: tianyuanapiCallLogService, + TianyuanapiClient: tianyuanapi, // 白名单相关模型 UserFeatureWhitelistModel: userFeatureWhitelistModel, WhitelistOrderModel: whitelistOrderModel, WhitelistOrderItemModel: whitelistOrderItemModel, + QueryWhitelistOpLogModel: queryWhitelistOpLogModel, // 订单相关模型 OrderModel: orderModel, diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 358a53f..2c7756a 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -197,6 +197,78 @@ type AdminCreateWhitelistResp struct { Id string `json:"id"` } +type AdminAppendQueryWhitelistReq struct { + Name string `json:"name,optional"` + IdCard string `json:"id_card"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark,optional"` +} + +type AdminAppendQueryWhitelistResp struct { + Code int `json:"code"` + Message string `json:"message"` + TransactionId string `json:"transaction_id"` + Entry *AdminQueryWhitelistEntry `json:"entry,optional"` +} + +type AdminCreateQueryWhitelistReq struct { + Name string `json:"name,optional"` + IdCard string `json:"id_card"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark,optional"` +} + +type AdminCreateQueryWhitelistResp struct { + Code int `json:"code"` + Message string `json:"message"` + TransactionId string `json:"transaction_id"` + Entry *AdminQueryWhitelistEntry `json:"entry,optional"` +} + +type AdminQueryWhitelistEntry struct { + Id string `json:"id"` + Name string `json:"name"` + IdCardMasked string `json:"id_card_masked"` + ApiCodes []string `json:"api_codes"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type AdminGetQueryWhitelistOpLogListReq struct { + Page int64 `form:"page"` + PageSize int64 `form:"pageSize"` + IdCard *string `form:"id_card,optional"` + Action *string `form:"action,optional"` + TianyuanCode *int64 `form:"tianyuan_code,optional"` +} + +type AdminQueryWhitelistOpLogItem struct { + Id string `json:"id"` + AdminUserId string `json:"admin_user_id"` + AdminUsername string `json:"admin_username"` + Action string `json:"action"` + ActionText string `json:"action_text"` + Name string `json:"name"` + IdCard string `json:"id_card"` + IdCardMasked string `json:"id_card_masked"` + ApiCodes []string `json:"api_codes"` + Remark string `json:"remark"` + TianyuanCode int64 `json:"tianyuan_code"` + TianyuanMessage string `json:"tianyuan_message"` + TransactionId string `json:"transaction_id"` + EntryId string `json:"entry_id"` + EntryStatus string `json:"entry_status"` + EntryApiCodes []string `json:"entry_api_codes"` + CreateTime string `json:"create_time"` +} + +type AdminGetQueryWhitelistOpLogListResp struct { + Total int64 `json:"total"` + Items []AdminQueryWhitelistOpLogItem `json:"items"` +} + type AdminDeleteApiReq struct { Id string `path:"id"` } diff --git a/app/main/model/queryWhitelistOpLogModel.go b/app/main/model/queryWhitelistOpLogModel.go new file mode 100644 index 0000000..992256d --- /dev/null +++ b/app/main/model/queryWhitelistOpLogModel.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ QueryWhitelistOpLogModel = (*customQueryWhitelistOpLogModel)(nil) + +type ( + // QueryWhitelistOpLogModel is an interface to be customized, add more methods here, + // and implement the added methods in customQueryWhitelistOpLogModel. + QueryWhitelistOpLogModel interface { + queryWhitelistOpLogModel + } + + customQueryWhitelistOpLogModel struct { + *defaultQueryWhitelistOpLogModel + } +) + +// NewQueryWhitelistOpLogModel returns a model for the database table. +func NewQueryWhitelistOpLogModel(conn sqlx.SqlConn, c cache.CacheConf) QueryWhitelistOpLogModel { + return &customQueryWhitelistOpLogModel{ + defaultQueryWhitelistOpLogModel: newQueryWhitelistOpLogModel(conn, c), + } +} diff --git a/app/main/model/queryWhitelistOpLogModel_gen.go b/app/main/model/queryWhitelistOpLogModel_gen.go new file mode 100644 index 0000000..528a5ba --- /dev/null +++ b/app/main/model/queryWhitelistOpLogModel_gen.go @@ -0,0 +1,398 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "reflect" + "time" + + "github.com/Masterminds/squirrel" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" + "ycc-server/common/globalkey" +) + +var ( + queryWhitelistOpLogFieldNames = builder.RawFieldNames(&QueryWhitelistOpLog{}) + queryWhitelistOpLogRows = strings.Join(queryWhitelistOpLogFieldNames, ",") + queryWhitelistOpLogRowsExpectAutoSet = strings.Join(stringx.Remove(queryWhitelistOpLogFieldNames, "`create_time`", "`update_time`"), ",") + queryWhitelistOpLogRowsWithPlaceHolder = strings.Join(stringx.Remove(queryWhitelistOpLogFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheYccQueryWhitelistOpLogIdPrefix = "cache:ycc:queryWhitelistOpLog:id:" +) + +type ( + queryWhitelistOpLogModel interface { + Insert(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) (sql.Result, error) + FindOne(ctx context.Context, id string) (*QueryWhitelistOpLog, error) + Update(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*QueryWhitelistOpLog, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryWhitelistOpLog, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryWhitelistOpLog, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*QueryWhitelistOpLog, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*QueryWhitelistOpLog, error) + Delete(ctx context.Context, session sqlx.Session, id string) error + } + + defaultQueryWhitelistOpLogModel struct { + sqlc.CachedConn + table string + } + + QueryWhitelistOpLog struct { + Id string `db:"id"` // UUID主键 + CreateTime time.Time `db:"create_time"` // 创建时间 + UpdateTime time.Time `db:"update_time"` // 更新时间 + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` // 删除状态:0=未删除,1=已删除 + Version int64 `db:"version"` // 版本号(乐观锁) + AdminUserId string `db:"admin_user_id"` // 操作管理员ID + Action string `db:"action"` // 操作类型:create=创建规则,append=追加接口 + Name string `db:"name"` // 姓名规则 + IdCard string `db:"id_card"` // 身份证号(明文,供后台审计) + IdCardMasked sql.NullString `db:"id_card_masked"` // 天远返回的脱敏身份证号 + ApiCodes string `db:"api_codes"` // 本次提交的产品编码(JSON数组) + Remark sql.NullString `db:"remark"` // 备注 + TianyuanCode int64 `db:"tianyuan_code"` // 天远 API 业务码 + TianyuanMessage sql.NullString `db:"tianyuan_message"` // 天远 API 返回描述 + TransactionId sql.NullString `db:"transaction_id"` // 天远 API 流水号 + EntryId sql.NullString `db:"entry_id"` // 天远规则ID(成功时) + EntryStatus sql.NullString `db:"entry_status"` // 规则状态(成功时) + EntryApiCodes sql.NullString `db:"entry_api_codes"` // 规则当前产品编码列表(JSON数组,成功时) + } +) + +func newQueryWhitelistOpLogModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultQueryWhitelistOpLogModel { + return &defaultQueryWhitelistOpLogModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`query_whitelist_op_log`", + } +} + +func (m *defaultQueryWhitelistOpLogModel) Insert(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + m.insertUUID(data) + yccQueryWhitelistOpLogIdKey := fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, queryWhitelistOpLogRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes) + } + return conn.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes) + }, yccQueryWhitelistOpLogIdKey) +} +func (m *defaultQueryWhitelistOpLogModel) insertUUID(data *QueryWhitelistOpLog) { + t := reflect.TypeOf(data).Elem() + v := reflect.ValueOf(data).Elem() + for i := 0; i < t.NumField(); i++ { + sf := t.Field(i) + if sf.Tag.Get("db") == "id" { + f := v.Field(i) + if f.IsValid() && f.CanSet() && f.Kind() == reflect.String { + if f.String() == "" { + f.SetString(uuid.NewString()) + } + } + break + } + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindOne(ctx context.Context, id string) (*QueryWhitelistOpLog, error) { + yccQueryWhitelistOpLogIdKey := fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, id) + var resp QueryWhitelistOpLog + err := m.QueryRowCtx(ctx, &resp, yccQueryWhitelistOpLogIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", queryWhitelistOpLogRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) Update(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) (sql.Result, error) { + yccQueryWhitelistOpLogIdKey := fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, queryWhitelistOpLogRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes, data.Id) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes, data.Id) + }, yccQueryWhitelistOpLogIdKey) +} + +func (m *defaultQueryWhitelistOpLogModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) error { + + oldVersion := data.Version + data.Version += 1 + + var sqlResult sql.Result + var err error + + yccQueryWhitelistOpLogIdKey := fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, data.Id) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, queryWhitelistOpLogRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes, data.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.AdminUserId, data.Action, data.Name, data.IdCard, data.IdCardMasked, data.ApiCodes, data.Remark, data.TianyuanCode, data.TianyuanMessage, data.TransactionId, data.EntryId, data.EntryStatus, data.EntryApiCodes, data.Id, oldVersion) + }, yccQueryWhitelistOpLogIdKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultQueryWhitelistOpLogModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *QueryWhitelistOpLog) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "QueryWhitelistOpLogModel delete err : %+v", err) + } + return nil +} + +func (m *defaultQueryWhitelistOpLogModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*QueryWhitelistOpLog, error) { + + builder = builder.Columns(queryWhitelistOpLogRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryWhitelistOpLog + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryWhitelistOpLog, error) { + + builder = builder.Columns(queryWhitelistOpLogRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryWhitelistOpLog + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryWhitelistOpLog, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(queryWhitelistOpLogRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*QueryWhitelistOpLog + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*QueryWhitelistOpLog, error) { + + builder = builder.Columns(queryWhitelistOpLogRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryWhitelistOpLog + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*QueryWhitelistOpLog, error) { + + builder = builder.Columns(queryWhitelistOpLogRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryWhitelistOpLog + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryWhitelistOpLogModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultQueryWhitelistOpLogModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultQueryWhitelistOpLogModel) Delete(ctx context.Context, session sqlx.Session, id string) error { + yccQueryWhitelistOpLogIdKey := fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, yccQueryWhitelistOpLogIdKey) + return err +} +func (m *defaultQueryWhitelistOpLogModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheYccQueryWhitelistOpLogIdPrefix, primary) +} +func (m *defaultQueryWhitelistOpLogModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", queryWhitelistOpLogRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultQueryWhitelistOpLogModel) tableName() string { + return m.table +} diff --git a/deploy/sql/admin_query_whitelist_menu_migration.sql b/deploy/sql/admin_query_whitelist_menu_migration.sql new file mode 100644 index 0000000..045cca3 --- /dev/null +++ b/deploy/sql/admin_query_whitelist_menu_migration.sql @@ -0,0 +1,75 @@ +-- ============================================ +-- 查询白名单 - 后台菜单迁移 SQL +-- 将「模块白名单」替换为「查询白名单」 +-- 数据库:ycc +-- 父菜单:产品管理 (48d0e129-a141-4a74-b519-7adc38d22d27) +-- ============================================ + +-- 1. 软删除旧「模块白名单」菜单 +UPDATE `admin_menu` +SET `del_state` = 1, `version` = `version` + 1 +WHERE `path` = '/product-manage/whitelist/list' AND `del_state` = 0; + +-- 2. 插入「查询白名单」菜单(已存在则跳过) +INSERT INTO `admin_menu` ( + `id`, `pid`, `name`, `path`, `component`, `redirect`, `meta`, + `status`, `type`, `sort`, `del_state`, `version` +) +SELECT + 'd02e094d-6af8-11f1-bd31-dec53e82fe75', + '48d0e129-a141-4a74-b519-7adc38d22d27', + 'queryWhitelist', + '/product-manage/query-whitelist/list', + '/product-manage/query-whitelist/list', + NULL, + JSON_OBJECT('icon', 'lucide:shield-off', 'title', '查询白名单'), + 1, + 1, + 0, + 0, + 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `admin_menu` + WHERE `path` = '/product-manage/query-whitelist/list' AND `del_state` = 0 +); + +-- 3. 给「超级管理员」授权新菜单 +INSERT INTO `admin_role_menu` (`id`, `role_id`, `menu_id`, `del_state`, `version`) +SELECT + UUID(), + '741b7a39-a95d-4b9d-8dc0-84ee664d5fef', + m.`id`, + 0, + 0 +FROM `admin_menu` m +WHERE m.`path` = '/product-manage/query-whitelist/list' + AND m.`del_state` = 0 + AND NOT EXISTS ( + SELECT 1 FROM `admin_role_menu` rm + WHERE rm.`role_id` = '741b7a39-a95d-4b9d-8dc0-84ee664d5fef' + AND rm.`menu_id` = m.`id` + AND rm.`del_state` = 0 + ); + +-- 4. 给所有曾拥有旧「模块白名单」权限的角色同步授权 +INSERT INTO `admin_role_menu` (`id`, `role_id`, `menu_id`, `del_state`, `version`) +SELECT + UUID(), + arm.`role_id`, + m.`id`, + 0, + 0 +FROM `admin_role_menu` arm +JOIN `admin_menu` old_m ON old_m.`path` = '/product-manage/whitelist/list' +JOIN `admin_menu` m ON m.`path` = '/product-manage/query-whitelist/list' AND m.`del_state` = 0 +WHERE arm.`menu_id` = old_m.`id` + AND arm.`del_state` = 0 + AND NOT EXISTS ( + SELECT 1 FROM `admin_role_menu` x + WHERE x.`role_id` = arm.`role_id` + AND x.`menu_id` = m.`id` + AND x.`del_state` = 0 + ); + +-- 执行后请重新登录管理后台 diff --git a/deploy/sql/admin_query_whitelist_op_log_menu_migration.sql b/deploy/sql/admin_query_whitelist_op_log_menu_migration.sql new file mode 100644 index 0000000..ed6fdd7 --- /dev/null +++ b/deploy/sql/admin_query_whitelist_op_log_menu_migration.sql @@ -0,0 +1,9 @@ +-- ============================================ +-- 移除独立的「查询白名单记录」菜单(记录已合并到配置页下方) +-- ============================================ + +UPDATE `admin_menu` +SET `del_state` = 1, `version` = `version` + 1 +WHERE `path` = '/product-manage/query-whitelist/op-log' AND `del_state` = 0; + +-- 执行后请重新登录管理后台 diff --git a/deploy/sql/query_whitelist_op_log_migration.sql b/deploy/sql/query_whitelist_op_log_migration.sql new file mode 100644 index 0000000..376d706 --- /dev/null +++ b/deploy/sql/query_whitelist_op_log_migration.sql @@ -0,0 +1,34 @@ +-- ============================================ +-- 查询白名单操作记录表 +-- 记录后台管理员通过天远 API 创建/追加屏蔽规则的操作 +-- ============================================ + +CREATE TABLE IF NOT EXISTS `query_whitelist_op_log` ( + `id` CHAR(36) NOT NULL COMMENT 'UUID主键', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT '0' COMMENT '删除状态:0=未删除,1=已删除', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号(乐观锁)', + + `admin_user_id` CHAR(36) NOT NULL COMMENT '操作管理员ID', + `action` varchar(20) NOT NULL COMMENT '操作类型:create=创建规则,append=追加接口', + `name` varchar(50) NOT NULL DEFAULT '*' COMMENT '姓名规则', + `id_card` varchar(50) NOT NULL COMMENT '身份证号(明文,供后台审计)', + `id_card_masked` varchar(50) DEFAULT NULL COMMENT '天远返回的脱敏身份证号', + `api_codes` varchar(2000) NOT NULL COMMENT '本次提交的产品编码(JSON数组)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `tianyuan_code` int NOT NULL DEFAULT 0 COMMENT '天远 API 业务码', + `tianyuan_message` varchar(500) DEFAULT NULL COMMENT '天远 API 返回描述', + `transaction_id` varchar(64) DEFAULT NULL COMMENT '天远 API 流水号', + `entry_id` varchar(64) DEFAULT NULL COMMENT '天远规则ID(成功时)', + `entry_status` varchar(20) DEFAULT NULL COMMENT '规则状态(成功时)', + `entry_api_codes` varchar(2000) DEFAULT NULL COMMENT '规则当前产品编码列表(JSON数组,成功时)', + + PRIMARY KEY (`id`), + KEY `idx_admin_user_id` (`admin_user_id`), + KEY `idx_id_card` (`id_card`), + KEY `idx_action` (`action`), + KEY `idx_tianyuan_code` (`tianyuan_code`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='查询白名单操作记录表'; diff --git a/tyapi.md b/tyapi.md new file mode 100644 index 0000000..679668b --- /dev/null +++ b/tyapi.md @@ -0,0 +1,324 @@ +# 查询白名单接口 — 下游对接说明 + +> 本文档供**已开通权限的 API 用户 / 合作方**对接使用。 +> 用于配置「查询为空」屏蔽规则:命中后,指定人员在指定接口上的查询将返回 `1000 查询为空`。 + +--- + +## 1. 服务地址 + +| 环境 | 基础地址 | +|------|----------| +| 生产 | `https://api.tianyuanapi.com` | + +完整路径 = 基础地址 + 下表路径,例如: + +`https://api.tianyuanapi.com/api/v1/query-whitelist/entries` + +--- + +## 2. 接口一览 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 创建规则 | POST | `/api/v1/query-whitelist/entries` | 首次为某身份证建立屏蔽规则 | +| 追加接口 | POST | `/api/v1/query-whitelist/entries/append` | 向已有规则追加产品编码(去重合并) | + +**说明:** + +- 规则仅对**当前 `Access-Id` 对应账号**生效。 +- 本接口为**配置类接口**,不产生业务查询、**不扣费**,也不会出现在常规 API 调用记录中。 +- 规则生效后,该账号调用对应产品接口时,若命中屏蔽条件,将返回 `1000 查询为空`。 + +--- + +## 3. 鉴权 + +每次请求需同时满足以下三项。 + +### 3.1 请求头 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `Access-Id` | string | 是 | 您的 API 账号 Access-Id | +| `Whitelist-Mgmt-Key` | string | 是 | 平台单独下发的**白名单管理密钥**(与 Access Key 不同,请向商务/技术支持索取) | +| `Content-Type` | string | 是 | 固定为 `application/json` | + +### 3.2 请求体加密 + +与业务 API 调用方式一致,外层仅传 `data`: + +```json +{ + "data": "" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `data` | string | 是 | 业务参数经 Access Key 加密后的 Base64 字符串 | + +- 使用您账号的 **Access Key**(16 进制字符串)进行 AES 加密。 +- Access Key **仅用于本地加密**,不要写入明文 JSON。 +- 服务端根据请求头 `Access-Id` 查找密钥并解密;**解密成功即完成身份校验**。 + +### 3.3 IP 白名单 + +调用方出口 IP 须已加入该账号在控制台配置的 IP 白名单(与业务 API 要求一致)。未在白名单内的 IP 将返回 `1004`。 + +--- + +## 4. 业务参数(加密前明文 JSON) + +创建与追加接口的明文字段相同: + +```json +{ + "name": "*", + "id_card": "350681198611130611", + "api_codes": ["FLXG0V4B", "JRZQ8A2D"], + "remark": "可选备注" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 姓名;填 `*` 表示只按身份证匹配,不校验姓名 | +| `id_card` | string | 是 | 18 位中国大陆身份证号 | +| `api_codes` | string[] | 是 | 生效的产品编码列表,见下表约束 | +| `remark` | string | 否 | 备注,最长 500 字符;追加接口传入非空时会覆盖原备注 | + +### 4.1 `api_codes` 约束 + +| 传参方式 | 是否允许 | 示例 | +|----------|----------|------| +| 字符串数组 | 允许 | `["FLXG0V4B", "JRZQ8A2D"]` | +| 单个字符串 | 不允许 | `"FLXG0V4B"` | +| 通配 `*` | 不允许 | `["*"]` | +| 空数组 | 不允许 | `[]` | +| 非字符串元素 | 不允许 | `[123]` | + +--- + +## 5. 创建与追加的区别 + +同一账号下,**同一身份证号 + 同一 `name`** 仅对应**一条**规则;多个生效接口配置在该规则的 `api_codes` 数组中。 + +### 5.1 创建 `POST /entries` + +| 情况 | 结果 | +|------|------| +| 尚无该身份证+姓名的规则 | 创建成功 | +| 规则已存在 | 失败,`1013 规则已存在`(不会自动合并) | + +### 5.2 追加 `POST /entries/append` + +| 情况 | 结果 | +|------|------| +| 规则已存在 | 成功;将本次 `api_codes` 与已有列表**去重合并**(保留原顺序,新编码追加在后) | +| 本次编码均已存在 | 仍返回成功(幂等),列表不变 | +| 规则不存在 | 失败,`1014 规则不存在,请先调用创建接口` | + +**示例:** + +``` +创建:api_codes = ["FLXG0V4B"] +追加:api_codes = ["JRZQ8A2D"] +→ 合并后为 ["FLXG0V4B", "JRZQ8A2D"](同一条规则,非新建) + +再次追加:api_codes = ["JRZQ8A2D", "FLXG2E8F"] +→ ["FLXG0V4B", "JRZQ8A2D", "FLXG2E8F"](JRZQ8A2D 已存在则跳过) +``` + +### 5.3 推荐流程 + +1. 首次配置 → 调用**创建接口**(可一次传齐全部 `api_codes`) +2. 后续增补产品 → 调用**追加接口**(只传新增编码) +3. 创建返回 `1013` → 改调**追加接口** + +--- + +## 6. 加密算法 + +与业务 API 完全一致: + +1. Access Key 为 16 进制字符串,解码为 16 字节 AES 密钥 +2. 采用 **AES-128-CBC**,**PKCS7** 填充 +3. 每次加密随机生成 16 字节 IV,置于密文前 +4. IV + 密文整体做 **Base64** 编码,放入 `data` + +**加密步骤:** + +``` +明文 JSON → AES-128-CBC 加密 → [IV(16字节) + 密文] → Base64 → 填入 data 字段 +``` + +**解密响应 `data` 时:** 取 Base64 解码后前 16 字节为 IV,余下为密文,用同一 Access Key 解密。 + +--- + +## 7. 响应格式 + +HTTP 状态码均为 `200`。以响应体 `code` 判断业务是否成功。 + +### 7.1 外层结构 + +```json +{ + "code": 0, + "message": "业务成功", + "transaction_id": "550e8400-e29b-41d4-a716-446655440000", + "data": "" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | int | `0` 表示成功,非 `0` 见错误码表 | +| `message` | string | 结果描述 | +| `transaction_id` | string | 本次请求流水号 | +| `data` | string | 成功时返回,为规则详情的 AES 密文,需用 Access Key 解密 | + +### 7.2 `data` 解密后结构 + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "*", + "id_card_masked": "350681********0611", + "api_codes": ["FLXG0V4B", "JRZQ8A2D"], + "status": "enabled", + "remark": "", + "created_at": "2026-06-18T10:00:00+08:00", + "updated_at": "2026-06-18T10:00:00+08:00" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 规则唯一标识 | +| `name` | string | 姓名规则 | +| `id_card_masked` | string | 脱敏身份证号 | +| `api_codes` | string[] | 当前生效的产品编码列表 | +| `status` | string | `enabled` 启用 / `disabled` 禁用 | +| `remark` | string | 备注 | +| `created_at` | string | 创建时间(ISO 8601) | +| `updated_at` | string | 最近更新时间(ISO 8601) | + +--- + +## 8. 错误码 + +| code | message | 说明 | +|------|---------|------| +| 0 | 业务成功 | 创建或追加成功 | +| 1001 | 接口异常 | 系统内部错误 | +| 1002 | 解密失败 | `data` 无法解密 | +| 1003 | 请求参数结构不正确 | 参数缺失、格式错误或 `api_codes` 不合法 | +| 1004 | 未经授权的IP | 调用 IP 不在账号白名单 | +| 1005 | 缺少Access-Id | 未传 `Access-Id` 请求头 | +| 1006 | 未经授权的AccessId | Access-Id 无效或账号已冻结 | +| 1010 | 缺少管理密钥 | 未传 `Whitelist-Mgmt-Key` | +| 1011 | 管理密钥无效 | 管理密钥错误 | +| 1012 | 接口未开放 | 功能未对该账号/环境开放 | +| 1013 | 规则已存在 | 创建时:该身份证+姓名规则已存在 | +| 1014 | 规则不存在 | 追加时:须先调用创建接口 | + +--- + +## 9. 调用示例(Node.js) + +```javascript +const crypto = require('crypto'); + +const BASE_URL = 'https://api.tianyuanapi.com'; +const ACCESS_ID = '您的Access-Id'; +const ACCESS_KEY = '您的AccessKey'; // 16 进制,用于加解密 +const MGMT_KEY = '平台下发的Whitelist-Mgmt-Key'; + +function encrypt(plainText, keyHex) { + const key = Buffer.from(keyHex, 'hex'); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + cipher.setAutoPadding(true); + let enc = cipher.update(plainText, 'utf8'); + enc = Buffer.concat([iv, enc, cipher.final()]); + return enc.toString('base64'); +} + +function decrypt(cipherBase64, keyHex) { + const key = Buffer.from(keyHex, 'hex'); + const buf = Buffer.from(cipherBase64, 'base64'); + const iv = buf.slice(0, 16); + const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + decipher.setAutoPadding(true); + let dec = decipher.update(buf.slice(16)); + dec = Buffer.concat([dec, decipher.final()]); + return dec.toString('utf8'); +} + +async function callWhitelistApi(path, payload) { + const res = await fetch(`${BASE_URL}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Id': ACCESS_ID, + 'Whitelist-Mgmt-Key': MGMT_KEY, + }, + body: JSON.stringify({ + data: encrypt(JSON.stringify(payload), ACCESS_KEY), + }), + }); + const json = await res.json(); + if (json.code === 0 && json.data) { + json.decrypted = JSON.parse(decrypt(json.data, ACCESS_KEY)); + } + return json; +} + +// 创建 +callWhitelistApi('/api/v1/query-whitelist/entries', { + name: '*', + id_card: '350681198611130611', + api_codes: ['FLXG0V4B'], + remark: '', +}).then(console.log); + +// 追加 +callWhitelistApi('/api/v1/query-whitelist/entries/append', { + name: '*', + id_card: '350681198611130611', + api_codes: ['JRZQ8A2D'], +}).then(console.log); +``` + +--- + +## 10. 常见问题 + +### `name` 填 `*` 是什么意思? + +只按**身份证号**匹配。只要业务请求中的身份证与该规则一致,无论传入什么姓名,均会返回「查询为空」。 + +### 与业务 API 的 Access Key 是什么关系? + +- **Access Key**:加密请求/响应,证明账号身份(与业务 API 相同)。 +- **Whitelist-Mgmt-Key**:平台另行下发的管理授权密钥,证明有权调用白名单配置接口。 + +两者缺一不可。 + +### 调用成功后,常规 API 调用记录里能看到吗? + +不能。本接口为配置类操作,不计入业务 API 调用次数,也不扣费。 + +### 屏蔽何时生效? + +接口返回 `code = 0` 后即生效(通常数秒内)。之后该账号调用 `api_codes` 中包含的产品且入参命中身份证(及姓名规则)时,将返回 `1000 查询为空`。 + +--- + +## 11. 联系方式 + +- **Access-Id / Access Key**:登录控制台 → API 密钥 +- **Whitelist-Mgmt-Key**:向平台商务或技术支持申请 +- **IP 白名单**:登录控制台 → API 设置中配置