package service import ( "bufio" "bytes" "context" "crypto/md5" "encoding/hex" "fmt" "image" "image/jpeg" "image/png" "io/ioutil" mrand "math/rand" "os" "strconv" "strings" "syscall" "time" "go-common/app/admin/main/member/conf" "go-common/app/admin/main/member/model" memmdl "go-common/app/service/main/member/model" "go-common/library/ecode" "go-common/library/log" "go-common/library/net/metadata" "go-common/library/queue/databus/report" "github.com/nfnt/resize" "github.com/pkg/errors" ) // consts const ( _512KiloBytes = 512 * 1024 ) // RealnameList . func (s *Service) RealnameList(ctx context.Context, arg *model.ArgRealnameList) (list []*model.RespRealnameApply, total int, err error) { list = make([]*model.RespRealnameApply, 0) switch arg.Channel { case model.ChannelMain: return s.realnameMainList(ctx, arg) case model.ChannelAlipay: return s.realnameAlipayList(ctx, arg) } return } func (s *Service) realnameMainList(ctx context.Context, arg *model.ArgRealnameList) (list []*model.RespRealnameApply, total int, err error) { var ( dl []*model.DBRealnameApply ) if arg.Card != "" { // 如果查询证件号, 则通过证件号MD5 realname_info 查询到 对应的 mids var ( infos []*model.DBRealnameInfo mids []int64 md5 = cardMD5(arg.Card, arg.DBCardType(), arg.Country) ) log.Info("realnameMainList card: %s, md5: %s", arg.Card, md5) if infos, err = s.dao.RealnameInfoByCardMD5(ctx, md5, arg.State.DBStatus(), model.ChannelMain.DBChannel()); err != nil { return } log.Info("realnameMainList infos : %+v", infos) if len(infos) <= 0 { return } for _, i := range infos { log.Info("realnameMainList info: %+v", i) mids = append(mids, i.MID) } log.Info("realnameMainList mids: %+v", mids) if dl, total, err = s.dao.RealnameMainList(ctx, mids, arg.DBCardType(), arg.DBCountry(), arg.OPName, arg.TSFrom, arg.TSTo, arg.DBState(), arg.PN, arg.PS, arg.IsDesc); err != nil { return } } else { var ( mids []int64 ) if arg.MID > 0 { mids = append(mids, arg.MID) } if dl, total, err = s.dao.RealnameMainList(ctx, mids, arg.DBCardType(), arg.DBCountry(), arg.OPName, arg.TSFrom, arg.TSTo, arg.DBState(), arg.PN, arg.PS, arg.IsDesc); err != nil { return } } var ( midMap = make(map[int64]int) // map[mid]count mids []int64 imgIDs []int64 ) // 审核 db 数据解析进 list for _, d := range dl { midMap[d.MID] = 0 var ( r = &model.RespRealnameApply{} ) r.ParseDBMainApply(d) imgIDs = append(imgIDs, d.HandIMG, d.FrontIMG, d.BackIMG) list = append(list, r) } // 没有数据则返回 if len(midMap) <= 0 { return } // 获取实名申请次数 for mid := range midMap { if midMap[mid], err = s.dao.RealnameApplyCount(ctx, mid); err != nil { return } mids = append(mids, mid) } // 获取mid的昵称 & 等级信息 var ( memsArg = &memmdl.ArgMemberMids{ Mids: mids, } memMap map[int64]*memmdl.Member imgMap map[int64]*model.DBRealnameApplyIMG ) if memMap, err = s.memberRPC.Members(ctx, memsArg); err != nil { err = errors.WithStack(err) return } // 获取证件照信息 if imgMap, err = s.dao.RealnameApplyIMG(ctx, imgIDs); err != nil { return } for _, ra := range list { if mem, ok := memMap[ra.MID]; ok { ra.ParseMember(mem) } for _, id := range ra.IMGIDs { if img, ok := imgMap[id]; ok { ra.ParseDBApplyIMG(img.IMGData) } } ra.Times = midMap[ra.MID] } return } func (s *Service) realnameAlipayList(ctx context.Context, arg *model.ArgRealnameList) (list []*model.RespRealnameApply, total int, err error) { var ( dl []*model.DBRealnameAlipayApply ) if arg.Card != "" { // 如果查询证件号, 则通过证件号MD5 realname_info 查询到 对应的 mids var ( infos []*model.DBRealnameInfo mids []int64 md5 = cardMD5(arg.Card, arg.DBCardType(), arg.Country) ) log.Info("realnameAlipayList card: %s, md5: %s", arg.Card, md5) if infos, err = s.dao.RealnameInfoByCardMD5(ctx, md5, arg.State.DBStatus(), model.ChannelAlipay.DBChannel()); err != nil { return } log.Info("realnameAlipayList infos : %+v", infos) if len(infos) <= 0 { return } for _, i := range infos { log.Info("realnameAlipayList info: %+v", i) mids = append(mids, i.MID) } log.Info("realnameAlipayList mids: %+v", mids) if dl, total, err = s.dao.RealnameAlipayList(ctx, mids, 0, 0, arg.State.DBStatus(), arg.PN, arg.PS, arg.IsDesc); err != nil { return } } else { var ( mids []int64 ) if arg.MID > 0 { mids = append(mids, arg.MID) } if dl, total, err = s.dao.RealnameAlipayList(ctx, mids, arg.TSFrom, arg.TSTo, arg.State.DBStatus(), arg.PN, arg.PS, arg.IsDesc); err != nil { return } } log.Info("realnameAlipayList dl: %+v, total: %d", dl, total) var ( midMap = make(map[int64]int) ) // append to list for _, d := range dl { midMap[d.MID] = 0 var ( r = &model.RespRealnameApply{} ) r.ParseDBAlipayApply(d) list = append(list, r) } if len(midMap) <= 0 { return } var mids []int64 for mid := range midMap { if midMap[mid], err = s.dao.RealnameApplyCount(ctx, mid); err != nil { return } mids = append(mids, mid) } var ( memsArg = &memmdl.ArgMemberMids{ Mids: mids, } memMap map[int64]*memmdl.Member ) if memMap, err = s.memberRPC.Members(ctx, memsArg); err != nil { err = errors.WithStack(err) return } for _, ra := range list { if mem, ok := memMap[ra.MID]; ok { ra.ParseMember(mem) } ra.Times = midMap[ra.MID] } return } func cardMD5(card string, cardType int, country int) (res string) { if card == "" || cardType < 0 || country < 0 { return } var ( lowerCode = strings.ToLower(card) key = fmt.Sprintf("%s_%s_%d_%d", model.RealnameSalt, lowerCode, cardType, country) ) return fmt.Sprintf("%x", md5.Sum([]byte(key))) } // RealnamePendingList . func (s *Service) RealnamePendingList(ctx context.Context, arg *model.ArgRealnamePendingList) (list []*model.RespRealnameApply, total int, err error) { var ( larg = &model.ArgRealnameList{ Channel: arg.Channel, State: model.RealnameApplyStatePending, TSFrom: time.Now().Add(-time.Hour * 24 * 7).Unix(), PS: arg.PS, PN: arg.PN, } ) return s.RealnameList(ctx, larg) } // RealnameAuditApply . func (s *Service) RealnameAuditApply(ctx context.Context, arg *model.ArgRealnameAuditApply, adminName string, adminID int64) (err error) { var ( mid int64 ) // 1. check the apply state switch arg.Channel { case model.ChannelMain: var apply *model.DBRealnameApply if apply, err = s.dao.RealnameMainApply(ctx, arg.ID); err != nil { return } if apply.IsPassed() { return } mid = apply.MID case model.ChannelAlipay: var apply *model.DBRealnameAlipayApply if apply, err = s.dao.RealnameAlipayApply(ctx, arg.ID); err != nil { return } if apply.Status == model.RealnameApplyStateNone.DBStatus() || apply.Status == model.RealnameApplyStateRejective.DBStatus() { return } mid = apply.MID } var ( state = 0 msgTitle = "" msgContent = "" mc = "2_2_1" expNotify = false ) switch arg.Action { case model.RealnameActionPass: state = model.RealnameApplyStatePassed.DBStatus() msgTitle = "您提交的实名认证已审核通过" msgContent = "恭喜,您提交的实名认证已通过审核" expNotify = true case model.RealnameActionReject: state = model.RealnameApplyStateRejective.DBStatus() msgTitle = "您提交的实名认证未通过审核" msgContent = fmt.Sprintf(`抱歉,您提交的实名认证未通过审核,驳回原因:%s。请修改后重新提交实名认证。`, arg.Reason) default: err = ecode.RequestErr return } // 2. do something switch arg.Channel { case model.ChannelMain: if err = s.dao.UpdateOldRealnameApply(ctx, arg.ID, state, adminName, adminID, time.Now(), arg.Reason); err != nil { return } case model.ChannelAlipay: if err = s.dao.UpdateRealnameAlipayApply(ctx, arg.ID, adminID, adminName, state, arg.Reason); err != nil { return } if err = s.dao.UpdateRealnameInfo(ctx, mid, state, arg.Reason); err != nil { return } } go func() { if err := s.dao.RawMessage(context.Background(), mc, msgTitle, msgContent, []int64{mid}); err != nil { log.Error("%+v", err) } if expNotify { expMsg := &model.AddExpMsg{ Event: "identify", Mid: mid, IP: metadata.String(ctx, metadata.RemoteIP), Ts: time.Now().Unix(), } if err := s.dao.PubExpMsg(ctx, expMsg); err != nil { log.Error("%+v", err) } } }() return } // RealnameReasonList . func (s *Service) RealnameReasonList(ctx context.Context, arg *model.ArgRealnameReasonList) (list []string, total int, err error) { return s.dao.RealnameReasonList(ctx) } // RealnameSetReason . func (s *Service) RealnameSetReason(ctx context.Context, arg *model.ArgRealnameSetReason) (err error) { return s.dao.UpdateRealnameReason(ctx, arg.Reasons) } // RealnameSearchCard . func (s *Service) RealnameSearchCard(ctx context.Context, cards []string, cardType int, country int) (data map[string]int64, err error) { var ( hashmap = make(map[string]string) //map[hash]card hashes = make([]string, 0) list []*model.DBRealnameInfo ) for _, card := range cards { hash := cardMD5(card, cardType, country) hashmap[hash] = card hashes = append(hashes, hash) } if list, err = s.dao.RealnameSearchCards(ctx, hashes); err != nil { return } data = make(map[string]int64) for _, l := range list { if rawCode, ok := hashmap[l.CardMD5]; ok { data[rawCode] = l.MID } } return } // RealnameUnbind is. func (s *Service) RealnameUnbind(ctx context.Context, mid int64, adminName string, adminID int64) (err error) { var ( info *model.DBRealnameInfo ) if info, err = s.dao.RealnameInfo(ctx, mid); err != nil { return } if info == nil { err = ecode.RealnameAlipayApplyInvalid return } if info.Status != model.RealnameApplyStatePassed.DBStatus() { return } if err = s.dao.UpdateRealnameInfo(ctx, mid, model.RealnameApplyStateRejective.DBStatus(), "管理后台解绑"); err != nil { return } switch info.Channel { case model.ChannelMain.DBChannel(): if err = s.dao.RejectRealnameMainApply(ctx, mid, adminName, adminID, "管理后台解绑"); err != nil { return } case model.ChannelAlipay.DBChannel(): if err = s.dao.RejectRealnameAlipayApply(ctx, mid, adminName, adminID, "管理后台解绑"); err != nil { return } default: log.Warn("Failed to reject realname apply: unrecognized channel: %+v", info) } go func() { r := &report.ManagerInfo{ Uname: adminName, UID: adminID, Business: model.RealnameManagerLogID, Type: 0, Oid: mid, Action: model.LogActionRealnameUnbind, Ctime: time.Now(), } if err = report.Manager(r); err != nil { log.Error("Send manager log failed : %+v , report : %+v", err, r) err = nil return } log.Info("Send manager log success report : %+v", r) }() return } // RealnameImage return img func (s *Service) RealnameImage(ctx context.Context, token string) ([]byte, error) { filePath := fmt.Sprintf("%s/%s.txt", conf.Conf.Realname.DataDir, token) _, err := os.Stat(filePath) if os.IsNotExist(err) { log.Info("file : %s , not found", filePath) return nil, ecode.RequestErr } file, err := os.Open(filePath) if err != nil { return nil, errors.WithStack(err) } defer file.Close() img, err := ioutil.ReadAll(file) if err != nil { return nil, errors.WithStack(err) } return s.mainCryptor.IMGDecrypt(img) } // FetchRealnameImage is func (s *Service) FetchRealnameImage(ctx context.Context, token string) ([]byte, error) { img, err := s.dao.GetRealnameImageCache(ctx, asIMGData(token)) if err == nil && len(img) > 0 { return img, nil } if err != nil { log.Warn("Failed to get realname image from cache: %s: %+v", token, err) } img, err = s.RealnameImage(ctx, token) if err != nil { return nil, err } if len(img) <= _512KiloBytes { return img, nil } striped, err := StripImage(img) if err != nil { log.Warn("Failed to strip image: %+v", err) return img, nil } return striped, nil } // RealnameImagePreview return preview img func (s *Service) RealnameImagePreview(ctx context.Context, token string, borderSize uint) (data []byte, err error) { var ( src []byte ) if src, err = s.RealnameImage(ctx, token); err != nil { return } if len(src) == 0 { return } var ( img image.Image imgWidth, imgHeight int imgFormat string sr = bytes.NewReader(src) ) if img, imgFormat, err = image.Decode(sr); err != nil { log.Warn("Failed to decode image: %+v, return origin image data directly", err) return src, nil } imgWidth, imgHeight = img.Bounds().Dx(), img.Bounds().Dy() log.Info("Decode img : %s , format : %s , width : %d , height : %d ", token, imgFormat, imgWidth, imgHeight) if imgFormat != "png" && imgFormat != "jpg" && imgFormat != "jpeg" { return } if imgWidth > imgHeight { img = resize.Resize(borderSize, 0, img, resize.Lanczos3) } else { img = resize.Resize(0, borderSize, img, resize.Lanczos3) } var ( bb bytes.Buffer bw = bufio.NewWriter(&bb) ) switch imgFormat { case "jpg", "jpeg": if err = jpeg.Encode(bw, img, nil); err != nil { err = errors.WithStack(err) return } case "png": if err = png.Encode(bw, img); err != nil { err = errors.WithStack(err) return } } data = bb.Bytes() return } // RealnameExcel export user realname info func (s *Service) RealnameExcel(ctx context.Context, mids []int64) ([]*model.RealnameExport, error) { infos, err := s.dao.BatchRealnameInfo(ctx, mids) if err != nil { log.Warn("Failed to get realname info with mids: %+v: %+v", mids, err) // keep an empty infos infos = make(map[int64]*model.DBRealnameInfo) } pinfos, err := s.dao.PassportQueryByMidsChunked(ctx, mids, 100) if err != nil { log.Warn("Failed to get passport query by mids: %+v: %+v", mids, err) // keep an empty infos pinfos = make(map[int64]*model.PassportQueryByMidResult) } res := make([]*model.RealnameExport, 0, len(mids)) for _, mid := range mids { export := &model.RealnameExport{ Mid: mid, } // passport func() { p, ok := pinfos[mid] if !ok { log.Warn("Failed to get passport info with mid: %d", mid) return } export.UserID = p.Userid export.Uname = p.Name export.Tel = p.Tel }() // realname func() { info, ok := infos[mid] if !ok { log.Warn("Failed to get realname info with mid: %d", mid) return } export.Realname = info.Realname export.CardType = info.CardType cardDecode, err := model.CardDecrypt(info.Card) if err != nil { log.Error("Failed to decrypt card: %s: %+v", info.Card, err) return } export.CardNum = cardDecode }() res = append(res, export) } return res, nil } // RealnameSubmit is func (s *Service) RealnameSubmit(ctx context.Context, arg *model.ArgRealnameSubmit) error { encryptedCardNum, err := s.realnameCrypto.CardEncrypt([]byte(arg.CardNum)) if err != nil { return err } _ = func() error { front := &model.DBRealnameApplyIMG{IMGData: asIMGData(arg.FrontImageToken)} if err := s.dao.AddRealnameIMG(ctx, front); err != nil { return err } back := &model.DBRealnameApplyIMG{IMGData: asIMGData(arg.BackImageToken)} if err := s.dao.AddRealnameIMG(ctx, back); err != nil { return err } apply := &model.DBRealnameApply{ MID: arg.Mid, Realname: arg.Realname, Country: arg.Country, CardType: arg.CardType, CardNum: string(encryptedCardNum), CardMD5: cardMD5(arg.CardNum, int(arg.CardType), int(arg.Country)), FrontIMG: front.ID, BackIMG: back.ID, Status: model.RealnameApplyStatePassed.DBStatus(), Operator: arg.Operator, OperatorID: arg.OperatorID, OperatorTime: time.Now(), } if arg.HandImageToken != "" { hand := &model.DBRealnameApplyIMG{IMGData: asIMGData(arg.HandImageToken)} if err := s.dao.AddRealnameIMG(ctx, hand); err != nil { return err } apply.HandIMG = hand.ID } if err := s.dao.AddRealnameApply(ctx, apply); err != nil { return err } info := &model.DBRealnameInfo{ MID: apply.MID, Channel: model.ChannelMain.DBChannel(), Realname: apply.Realname, Country: apply.Country, CardType: apply.CardType, Card: apply.CardNum, CardMD5: apply.CardMD5, Status: model.RealnameApplyStatePassed.DBStatus(), Reason: fmt.Sprintf("管理后台提交:%s", arg.Remark), } return s.dao.SubmitRealnameInfo(ctx, info) } toOld := func() error { front := &model.DeDeIdentificationCardApplyImg{IMGData: asIMGData(arg.FrontImageToken)} if err := s.dao.AddOldRealnameIMG(ctx, front); err != nil { return err } back := &model.DeDeIdentificationCardApplyImg{IMGData: asIMGData(arg.BackImageToken)} if err := s.dao.AddOldRealnameIMG(ctx, back); err != nil { return err } apply := &model.DeDeIdentificationCardApply{ MID: arg.Mid, Realname: arg.Realname, Type: arg.CardType, CardData: string(encryptedCardNum), CardForSearch: cardMD5(arg.CardNum, int(arg.CardType), int(arg.Country)), FrontImg: front.ID, BackImg: back.ID, ApplyTime: int32(time.Now().Unix()), Operator: arg.Operator, OperatorTime: int32(time.Now().Unix()), Status: int8(model.RealnameApplyStatePassed.DBStatus()), Remark: fmt.Sprintf("管理后台提交:%s", arg.Remark), } if arg.HandImageToken != "" { hand := &model.DeDeIdentificationCardApplyImg{IMGData: asIMGData(arg.HandImageToken)} if err := s.dao.AddOldRealnameIMG(ctx, hand); err != nil { return err } apply.FrontImg2 = hand.ID } if err := s.dao.AddOldRealnameApply(ctx, apply); err != nil { return err } info := &model.DBRealnameInfo{ MID: apply.MID, Channel: model.ChannelMain.DBChannel(), Realname: apply.Realname, Country: arg.Country, CardType: arg.CardType, Card: apply.CardData, CardMD5: apply.CardForSearch, Status: model.RealnameApplyStatePassed.DBStatus(), Reason: fmt.Sprintf("管理后台提交:%s", arg.Remark), } return s.dao.SubmitRealnameInfo(ctx, info) } if err := toOld(); err != nil { return err } report.Manager(&report.ManagerInfo{ Uname: arg.Operator, UID: arg.OperatorID, Business: model.RealnameManagerLogID, Type: 0, Oid: arg.Mid, Action: model.LogActionRealnameSubmit, Ctime: time.Now(), }) return nil } // RealnameFileUpload is func (s *Service) RealnameFileUpload(ctx context.Context, mid int64, data []byte) (src string, err error) { var ( md5Engine = md5.New() hashMID string hashRand string fileName string dirPath string dateStr string ) md5Engine.Write([]byte(strconv.FormatInt(mid, 10))) hashMID = hex.EncodeToString(md5Engine.Sum(nil)) md5Engine.Reset() md5Engine.Write([]byte(strconv.FormatInt(time.Now().Unix(), 10))) md5Engine.Write([]byte(strconv.FormatInt(mrand.Int63n(1000000), 10))) hashRand = hex.EncodeToString(md5Engine.Sum(nil)) fileName = fmt.Sprintf("%s_%s.txt", hashMID[:6], hashRand) dateStr = time.Now().Format("20060102") dirPath = fmt.Sprintf("%s/%s/", s.c.Realname.DataDir, dateStr) var ( dataFile *os.File writeFileSize int encrptedData []byte ) _, err = os.Stat(dirPath) if os.IsNotExist(err) { mask := syscall.Umask(0) defer syscall.Umask(mask) if err = os.MkdirAll(dirPath, 0777); err != nil { err = errors.WithStack(err) return } } if encrptedData, err = s.mainCryptor.IMGEncrypt(data); err != nil { err = errors.WithStack(err) return } if dataFile, err = os.Create(dirPath + fileName); err != nil { err = errors.Wrapf(err, "create file %s failed", dirPath+fileName) return } defer dataFile.Close() if writeFileSize, err = dataFile.Write(encrptedData); err != nil { err = errors.Wrapf(err, "write file %s size %d failed", dirPath+fileName, len(encrptedData)) return } if writeFileSize != len(encrptedData) { err = errors.Errorf("Write file data to %s , expected %d actual %d", dirPath+fileName, len(encrptedData), writeFileSize) return } src = fmt.Sprintf("%s/%s", dateStr, strings.TrimSuffix(fileName, ".txt")) return } func asIMGData(imgToken string) string { return model.RealnameImgPrefix + imgToken + model.RealnameImgSuffix } func asIMGToken(IMGData string) string { token := strings.TrimPrefix(IMGData, "/idenfiles/") token = strings.TrimSuffix(token, ".txt") return token } // StripImage is func StripImage(raw []byte) ([]byte, error) { i, format, err := image.Decode(bytes.NewReader(raw)) if err != nil { return nil, errors.WithStack(err) } out := &bytes.Buffer{} switch format { case "jpg", "jpeg": if err := jpeg.Encode(out, i, &jpeg.Options{Quality: jpeg.DefaultQuality}); err != nil { return nil, errors.WithStack(err) } default: return nil, errors.Errorf("Unsupported type: %s", format) } return out.Bytes(), nil }