package service import ( "context" "database/sql" "encoding/json" "regexp" "strings" "ycc-server/app/main/api/internal/config" 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 queryWhitelistIdCardPattern = regexp.MustCompile(`^\d{17}[\dXx]$`) const ( tianyuanQueryWhitelistSuccessCode = 0 tianyuanQueryWhitelistExistsCode = 1013 tianyuanQueryWhitelistNotFoundCode = 1014 tianyuanQueryWhitelistLocalFailCode = -1 ) // QueryWhitelistSyncService 天远查询白名单同步服务 type QueryWhitelistSyncService struct { config config.Config client *tianyuanapi.Client opLogModel model.QueryWhitelistOpLogModel } func NewQueryWhitelistSyncService( c config.Config, client *tianyuanapi.Client, opLogModel model.QueryWhitelistOpLogModel, ) *QueryWhitelistSyncService { return &QueryWhitelistSyncService{ config: c, client: client, opLogModel: opLogModel, } } // TrySync 尽力同步天远查询白名单,失败仅记操作日志,不影响主流程 func (s *QueryWhitelistSyncService) TrySync( ctx context.Context, name, idCard string, apiCodes []string, remark string, ) { if err := s.Sync(ctx, name, idCard, apiCodes, remark); err != nil { logx.WithContext(ctx).Errorf("天远查询白名单同步失败(不影响主流程): %v", err) } } // TrySyncWhitelistOrder 尽力同步白名单订单中的模块到天远 func (s *QueryWhitelistSyncService) TrySyncWhitelistOrder( ctx context.Context, whitelistOrder *model.WhitelistOrder, items []*model.WhitelistOrderItem, remark string, ) { if whitelistOrder == nil || len(items) == 0 { return } s.TrySync(ctx, "*", whitelistOrder.IdCard, collectWhitelistOrderApiCodes(items), remark) } // Sync 将产品编码同步到天远查询白名单(先追加,规则不存在则创建) func (s *QueryWhitelistSyncService) Sync( ctx context.Context, name, idCard string, apiCodes []string, remark string, ) error { if err := validateQueryWhitelistSyncReq(idCard, apiCodes); err != nil { s.recordOpLogFailure(ctx, "sync", name, idCard, apiCodes, remark, err.Error(), tianyuanQueryWhitelistLocalFailCode) return err } appendResp, err := s.callMgmt( "/api/v1/query-whitelist/entries/append", name, idCard, apiCodes, remark, ) if err != nil { s.recordOpLogFailure(ctx, "append", name, idCard, apiCodes, remark, err.Error(), tianyuanQueryWhitelistLocalFailCode) return err } if isTianyuanQueryWhitelistSuccess(appendResp.Code) { s.recordOpLog(ctx, "append", name, idCard, apiCodes, remark, appendResp) return nil } if appendResp.Code != tianyuanQueryWhitelistNotFoundCode { s.recordOpLog(ctx, "append", name, idCard, apiCodes, remark, appendResp) return errors.Wrapf(xerr.NewErrMsg(appendResp.Message), "天远查询白名单同步失败(code=%d)", appendResp.Code) } createResp, err := s.callMgmt( "/api/v1/query-whitelist/entries", name, idCard, apiCodes, remark, ) if err != nil { s.recordOpLogFailure(ctx, "create", name, idCard, apiCodes, remark, err.Error(), tianyuanQueryWhitelistLocalFailCode) return err } if isTianyuanQueryWhitelistSuccess(createResp.Code) { s.recordOpLog(ctx, "create", name, idCard, apiCodes, remark, createResp) return nil } if createResp.Code == tianyuanQueryWhitelistExistsCode { retryResp, retryErr := s.callMgmt( "/api/v1/query-whitelist/entries/append", name, idCard, apiCodes, remark, ) if retryErr != nil { s.recordOpLogFailure(ctx, "append", name, idCard, apiCodes, remark, retryErr.Error(), tianyuanQueryWhitelistLocalFailCode) return retryErr } s.recordOpLog(ctx, "append", name, idCard, apiCodes, remark, retryResp) if isTianyuanQueryWhitelistSuccess(retryResp.Code) { return nil } return errors.Wrapf(xerr.NewErrMsg(retryResp.Message), "天远查询白名单同步失败(code=%d)", retryResp.Code) } s.recordOpLog(ctx, "create", name, idCard, apiCodes, remark, createResp) return errors.Wrapf(xerr.NewErrMsg(createResp.Message), "天远查询白名单同步失败(code=%d)", createResp.Code) } func collectWhitelistOrderApiCodes(items []*model.WhitelistOrderItem) []string { codes := make([]string, 0, len(items)) seen := make(map[string]bool, len(items)) for _, item := range items { if item == nil || item.FeatureApiId == "" || seen[item.FeatureApiId] { continue } seen[item.FeatureApiId] = true codes = append(codes, item.FeatureApiId) } return codes } func validateQueryWhitelistSyncReq(idCard string, apiCodes []string) error { if strings.TrimSpace(idCard) == "" { return errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "") } if !queryWhitelistIdCardPattern.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 (s *QueryWhitelistSyncService) callMgmt( apiPath string, name, idCard string, apiCodes []string, remark string, ) (*tianyuanapi.QueryWhitelistMgmtResponse, error) { mgmtKey := s.config.Tianyuanapi.WhitelistMgmtKey if mgmtKey == "" { return nil, errors.Wrapf(xerr.NewErrMsg("查询白名单管理密钥未配置,请在服务端配置 Tianyuanapi.WhitelistMgmtKey"), "") } if s.client == nil { return nil, errors.Wrapf(xerr.NewErrMsg("天远 API 客户端未初始化"), "") } if strings.TrimSpace(name) == "" { name = "*" } payload := map[string]interface{}{ "name": name, "id_card": strings.TrimSpace(idCard), "api_codes": apiCodes, } if strings.TrimSpace(remark) != "" { payload["remark"] = strings.TrimSpace(remark) } return s.client.CallQueryWhitelistMgmt(apiPath, payload, mgmtKey) } func (s *QueryWhitelistSyncService) recordOpLogFailure( ctx context.Context, action, name, idCard string, apiCodes []string, remark, message string, code int64, ) { s.insertOpLog(ctx, action, name, idCard, apiCodes, remark, code, message, "", nil) } func (s *QueryWhitelistSyncService) recordOpLog( ctx context.Context, action, name, idCard string, apiCodes []string, remark string, apiResp *tianyuanapi.QueryWhitelistMgmtResponse, ) { if apiResp == nil { return } s.insertOpLog(ctx, action, name, idCard, apiCodes, remark, int64(apiResp.Code), apiResp.Message, apiResp.TransactionID, apiResp.Entry) } func (s *QueryWhitelistSyncService) insertOpLog( ctx context.Context, action, name, idCard string, apiCodes []string, remark string, tianyuanCode int64, tianyuanMessage, transactionID string, entry *tianyuanapi.QueryWhitelistEntry, ) { if s.opLogModel == nil { return } operatorUserId, err := ctxdata.GetUidFromCtx(ctx) if err != nil { logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: 获取操作人ID失败, %v", err) operatorUserId = "" } apiCodesJSON, marshalErr := json.Marshal(apiCodes) if marshalErr != nil { logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: 序列化 api_codes 失败, %v", marshalErr) return } log := &model.QueryWhitelistOpLog{ AdminUserId: operatorUserId, Action: action, Name: resolveQueryWhitelistName(name), IdCard: strings.TrimSpace(idCard), ApiCodes: string(apiCodesJSON), TianyuanCode: tianyuanCode, } if strings.TrimSpace(remark) != "" { log.Remark = sql.NullString{String: strings.TrimSpace(remark), Valid: true} } if tianyuanMessage != "" { log.TianyuanMessage = sql.NullString{String: tianyuanMessage, Valid: true} } if transactionID != "" { log.TransactionId = sql.NullString{String: transactionID, Valid: true} } if entry != nil { if entry.IdCardMasked != "" { log.IdCardMasked = sql.NullString{String: entry.IdCardMasked, Valid: true} } if entry.ID != "" { log.EntryId = sql.NullString{String: entry.ID, Valid: true} } if entry.Status != "" { log.EntryStatus = sql.NullString{String: entry.Status, Valid: true} } if len(entry.ApiCodes) > 0 { entryCodesJSON, _ := json.Marshal(entry.ApiCodes) log.EntryApiCodes = sql.NullString{String: string(entryCodesJSON), Valid: true} } } if _, insertErr := s.opLogModel.Insert(ctx, nil, log); insertErr != nil { logx.WithContext(ctx).Errorf("记录查询白名单操作日志失败: %v", insertErr) } } func resolveQueryWhitelistName(name string) string { if strings.TrimSpace(name) == "" { return "*" } return strings.TrimSpace(name) } func isTianyuanQueryWhitelistSuccess(code int) bool { return code == tianyuanQueryWhitelistSuccessCode }