go-common/app/admin/main/member/service/realname.go

773 lines
21 KiB
Go
Raw Normal View History

2019-04-22 10:49:16 +00:00
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
}