package service import ( "context" "database/sql" "encoding/json" "fmt" "time" "ycc-server/app/main/api/internal/config" "ycc-server/app/main/model" "ycc-server/pkg/lzkit/lzUtils" "github.com/google/uuid" "github.com/pkg/errors" "github.com/smartwalle/alipay/v3" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/sqlx" ) // AlipayComplaintService 支付宝投诉服务,集中处理投诉相关的业务逻辑 type AlipayComplaintService struct { config config.Config AlipayClient *alipay.Client ComplaintMainModel model.ComplaintMainModel ComplaintAlipayModel model.ComplaintAlipayModel ComplaintAlipayTradeModel model.ComplaintAlipayTradeModel OrderModel model.OrderModel // 用于订单关联 } // NewAlipayComplaintService 创建支付宝投诉服务 func NewAlipayComplaintService( c config.Config, alipayClient *alipay.Client, complaintMainModel model.ComplaintMainModel, complaintAlipayModel model.ComplaintAlipayModel, complaintAlipayTradeModel model.ComplaintAlipayTradeModel, orderModel model.OrderModel, ) *AlipayComplaintService { return &AlipayComplaintService{ config: c, AlipayClient: alipayClient, ComplaintMainModel: complaintMainModel, ComplaintAlipayModel: complaintAlipayModel, ComplaintAlipayTradeModel: complaintAlipayTradeModel, OrderModel: orderModel, } } // QueryComplaintDetail 查询投诉详情 func (s *AlipayComplaintService) QueryComplaintDetail(ctx context.Context, complainId int64) (*alipay.SecurityRiskComplaintInfo, error) { req := alipay.SecurityRiskComplaintInfoQueryReq{ ComplainId: complainId, } resp, err := s.AlipayClient.SecurityRiskComplaintInfoQuery(ctx, req) if err != nil { return nil, errors.Wrapf(err, "查询投诉详情失败, complain_id: %d", complainId) } if !resp.IsSuccess() { return nil, errors.Errorf("支付宝返回错误: %s-%s", resp.Code, resp.Msg) } return &resp.SecurityRiskComplaintInfo, nil } // QueryComplaintByTaskId 根据 task_id 查询投诉详情并更新记录 func (s *AlipayComplaintService) QueryComplaintByTaskId(ctx context.Context, taskId string) error { // 1. 通过 task_id 查找数据库中的投诉记录 complaint, err := s.ComplaintAlipayModel.FindOneByTaskId(ctx, taskId) if err != nil { if err == model.ErrNotFound { logx.Infof("投诉记录不存在, task_id: %s,可能是新投诉,将在定时任务中同步", taskId) return nil // 新投诉会在定时任务中同步,这里不处理 } return errors.Wrapf(err, "查找投诉记录失败, task_id: %s", taskId) } // 2. 使用 alipay_id 查询投诉详情 detail, err := s.QueryComplaintDetail(ctx, complaint.AlipayId) if err != nil { return errors.Wrapf(err, "查询投诉详情失败, task_id: %s, alipay_id: %d", taskId, complaint.AlipayId) } logx.Infof("查询投诉详情成功, task_id: %s, alipay_id: %d, status: %s", taskId, complaint.AlipayId, detail.Status) // 3. 更新投诉记录 return s.UpdateComplaintFromDetail(ctx, complaint, detail) } // UpdateComplaintFromDetail 根据查询详情更新投诉记录 func (s *AlipayComplaintService) UpdateComplaintFromDetail(ctx context.Context, complaint *model.ComplaintAlipay, detail *alipay.SecurityRiskComplaintInfo) error { return s.ComplaintAlipayModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { // 1. 更新或创建投诉主表记录 var complaintMain *model.ComplaintMain var err error if complaint.ComplaintId != "" { // 查找主表记录 complaintMain, err = s.ComplaintMainModel.FindOne(transCtx, complaint.ComplaintId) if err != nil { return errors.Wrapf(err, "查找投诉主表失败, complaint_id: %s", complaint.ComplaintId) } // 更新主表信息 complaintMain.Name = lzUtils.StringToNullString(detail.OppositeName) // 使用被投诉方名称作为投诉人姓名 complaintMain.Contact = lzUtils.StringToNullString(detail.Contact) complaintMain.Content = lzUtils.StringToNullString(detail.ComplainContent) complaintMain.Status = lzUtils.StringToNullString(detail.Status) complaintMain.StatusDescription = lzUtils.StringToNullString(detail.StatusDescription) // 如果主表没有订单ID,尝试关联订单 if !complaintMain.OrderId.Valid { if detail.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, detail.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) } } // 尝试从交易信息中查找 if !complaintMain.OrderId.Valid && len(detail.ComplaintTradeInfoList) > 0 { for _, tradeInfo := range detail.ComplaintTradeInfoList { if tradeInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, tradeInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) break } } if tradeInfo.OutNo != "" { order, err := s.OrderModel.FindOneByOrderNo(transCtx, tradeInfo.OutNo) if err == nil && order != nil { complaintMain.OrderId = lzUtils.StringToNullString(order.Id) break } } } } } if err := s.ComplaintMainModel.UpdateWithVersion(transCtx, session, complaintMain); err != nil { return errors.Wrapf(err, "更新投诉主表失败, complaint_id: %s", complaint.ComplaintId) } } else { // 创建新的主表记录 complaintMain = &model.ComplaintMain{ Id: uuid.NewString(), Type: "alipay", Name: lzUtils.StringToNullString(detail.OppositeName), Contact: lzUtils.StringToNullString(detail.Contact), Content: lzUtils.StringToNullString(detail.ComplainContent), Status: lzUtils.StringToNullString(detail.Status), StatusDescription: lzUtils.StringToNullString(detail.StatusDescription), } // 如果有交易单号,尝试关联订单 if detail.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, detail.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) } } // 如果主表没有订单ID,尝试从交易信息中查找 if !complaintMain.OrderId.Valid && len(detail.ComplaintTradeInfoList) > 0 { for _, tradeInfo := range detail.ComplaintTradeInfoList { if tradeInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, tradeInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) break } } // 也可以尝试通过 out_no(商家订单号)查找 if tradeInfo.OutNo != "" { order, err := s.OrderModel.FindOneByOrderNo(transCtx, tradeInfo.OutNo) if err == nil && order != nil { complaintMain.OrderId = lzUtils.StringToNullString(order.Id) break } } } } if _, err := s.ComplaintMainModel.Insert(transCtx, session, complaintMain); err != nil { return errors.Wrapf(err, "创建投诉主表失败, task_id: %s", complaint.TaskId) } complaint.ComplaintId = complaintMain.Id } // 2. 更新支付宝投诉表信息 complaint.OppositePid = lzUtils.StringToNullString(detail.OppositePid) complaint.OppositeName = lzUtils.StringToNullString(detail.OppositeName) if detail.ComplainAmount != "" { amount, err := s.parseDecimal(detail.ComplainAmount) if err == nil { complaint.ComplainAmount = lzUtils.Float64ToNullFloat64(amount) } } complaint.GmtComplain = s.parseTime(detail.GmtComplain) complaint.GmtProcess = s.parseTime(detail.GmtProcess) complaint.GmtOverdue = s.parseTime(detail.GmtOverdue) complaint.ComplainContent = lzUtils.StringToNullString(detail.ComplainContent) complaint.TradeNo = lzUtils.StringToNullString(detail.TradeNo) complaint.Status = lzUtils.StringToNullString(detail.Status) complaint.StatusDescription = lzUtils.StringToNullString(detail.StatusDescription) complaint.ProcessCode = lzUtils.StringToNullString(detail.ProcessCode) complaint.ProcessMessage = lzUtils.StringToNullString(detail.ProcessMessage) complaint.ProcessRemark = lzUtils.StringToNullString(detail.ProcessRemark) complaint.GmtRiskFinishTime = s.parseTime(detail.GmtRiskFinishTime) complaint.ComplainUrl = lzUtils.StringToNullString(detail.ComplainUrl) // 处理图片列表 if len(detail.ProcessImgUrlList) > 0 { imgJson, _ := json.Marshal(detail.ProcessImgUrlList) complaint.ProcessImgUrlList = lzUtils.StringToNullString(string(imgJson)) } if len(detail.CertifyInfo) > 0 { certifyJson, _ := json.Marshal(detail.CertifyInfo) complaint.CertifyInfo = lzUtils.StringToNullString(string(certifyJson)) } // 更新支付宝投诉表 if err := s.ComplaintAlipayModel.UpdateWithVersion(transCtx, session, complaint); err != nil { return errors.Wrapf(err, "更新支付宝投诉表失败, task_id: %s", complaint.TaskId) } // 3. 更新交易信息:先删除旧的,再插入新的 oldTrades, _ := s.ComplaintAlipayTradeModel.FindAll(transCtx, s.ComplaintAlipayTradeModel.SelectBuilder(). Where("complaint_id = ? AND del_state = ?", complaint.Id, 0), "") for _, oldTrade := range oldTrades { oldTrade.DelState = 1 oldTrade.DeleteTime = lzUtils.TimeToNullTime(time.Now()) s.ComplaintAlipayTradeModel.UpdateWithVersion(transCtx, session, oldTrade) } // 插入新的交易信息 for _, tradeInfo := range detail.ComplaintTradeInfoList { trade := &model.ComplaintAlipayTrade{ Id: uuid.NewString(), ComplaintId: complaint.Id, AlipayTradeId: lzUtils.Int64ToNullInt64(tradeInfo.Id), AlipayComplaintRecordId: lzUtils.Int64ToNullInt64(tradeInfo.ComplaintRecordId), TradeNo: lzUtils.StringToNullString(tradeInfo.TradeNo), OutNo: lzUtils.StringToNullString(tradeInfo.OutNo), GmtTrade: s.parseTime(tradeInfo.GmtTrade), GmtRefund: s.parseTime(tradeInfo.GmtRefund), Status: lzUtils.StringToNullString(tradeInfo.Status), StatusDescription: lzUtils.StringToNullString(tradeInfo.StatusDescription), } if tradeInfo.Amount != "" { amount, err := s.parseDecimal(tradeInfo.Amount) if err == nil { trade.Amount = lzUtils.Float64ToNullFloat64(amount) } } if _, err := s.ComplaintAlipayTradeModel.Insert(transCtx, session, trade); err != nil { return errors.Wrapf(err, "插入投诉交易信息失败, task_id: %s", complaint.TaskId) } } return nil }) } // SaveComplaint 保存投诉数据到数据库(用于定时任务同步) func (s *AlipayComplaintService) SaveComplaint(ctx context.Context, complaintInfo *alipay.SecurityRiskComplaintInfo) error { // 检查投诉是否已存在(通过 task_id) existing, err := s.ComplaintAlipayModel.FindOneByTaskId(ctx, complaintInfo.TaskId) if err != nil && err != model.ErrNotFound { return errors.Wrapf(err, "查询投诉失败, task_id: %s", complaintInfo.TaskId) } return s.ComplaintAlipayModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { var complaintRecord *model.ComplaintAlipay var complaintMain *model.ComplaintMain var isUpdate bool if existing != nil { // 更新现有投诉 complaintRecord = existing isUpdate = true // 查找主表记录 if complaintRecord.ComplaintId != "" { complaintMain, err = s.ComplaintMainModel.FindOne(transCtx, complaintRecord.ComplaintId) if err != nil { return errors.Wrapf(err, "查找投诉主表失败, complaint_id: %s", complaintRecord.ComplaintId) } } } else { // 创建新投诉 complaintRecord = &model.ComplaintAlipay{ Id: uuid.NewString(), } isUpdate = false } // 1. 创建或更新投诉主表记录 if complaintMain == nil { // 创建新的主表记录 complaintMain = &model.ComplaintMain{ Id: uuid.NewString(), Type: "alipay", Name: lzUtils.StringToNullString(complaintInfo.OppositeName), Contact: lzUtils.StringToNullString(complaintInfo.Contact), Content: lzUtils.StringToNullString(complaintInfo.ComplainContent), Status: lzUtils.StringToNullString(complaintInfo.Status), StatusDescription: lzUtils.StringToNullString(complaintInfo.StatusDescription), } // 如果有交易单号,尝试关联订单 if complaintInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, complaintInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) } } // 如果主表没有订单ID,尝试从交易信息中查找 if !complaintMain.OrderId.Valid && len(complaintInfo.ComplaintTradeInfoList) > 0 { for _, tradeInfo := range complaintInfo.ComplaintTradeInfoList { if tradeInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, tradeInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) break } } // 也可以尝试通过 out_no(商家订单号)查找 if tradeInfo.OutNo != "" { order, err := s.OrderModel.FindOneByOrderNo(transCtx, tradeInfo.OutNo) if err == nil && order != nil { complaintMain.OrderId = lzUtils.StringToNullString(order.Id) break } } } } if _, err := s.ComplaintMainModel.Insert(transCtx, session, complaintMain); err != nil { return errors.Wrapf(err, "创建投诉主表失败, task_id: %s", complaintInfo.TaskId) } complaintRecord.ComplaintId = complaintMain.Id } else { // 更新主表信息 complaintMain.Name = lzUtils.StringToNullString(complaintInfo.OppositeName) complaintMain.Contact = lzUtils.StringToNullString(complaintInfo.Contact) complaintMain.Content = lzUtils.StringToNullString(complaintInfo.ComplainContent) complaintMain.Status = lzUtils.StringToNullString(complaintInfo.Status) complaintMain.StatusDescription = lzUtils.StringToNullString(complaintInfo.StatusDescription) // 如果主表没有订单ID,尝试关联订单 if !complaintMain.OrderId.Valid { if complaintInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, complaintInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) } } // 尝试从交易信息中查找 if !complaintMain.OrderId.Valid && len(complaintInfo.ComplaintTradeInfoList) > 0 { for _, tradeInfo := range complaintInfo.ComplaintTradeInfoList { if tradeInfo.TradeNo != "" { orderId := s.findOrderByPlatformOrderId(transCtx, tradeInfo.TradeNo) if orderId != "" { complaintMain.OrderId = lzUtils.StringToNullString(orderId) break } } if tradeInfo.OutNo != "" { order, err := s.OrderModel.FindOneByOrderNo(transCtx, tradeInfo.OutNo) if err == nil && order != nil { complaintMain.OrderId = lzUtils.StringToNullString(order.Id) break } } } } } if err := s.ComplaintMainModel.UpdateWithVersion(transCtx, session, complaintMain); err != nil { return errors.Wrapf(err, "更新投诉主表失败, task_id: %s", complaintInfo.TaskId) } } // 2. 填充支付宝投诉表数据 complaintRecord.AlipayId = complaintInfo.Id complaintRecord.TaskId = complaintInfo.TaskId complaintRecord.OppositePid = lzUtils.StringToNullString(complaintInfo.OppositePid) complaintRecord.OppositeName = lzUtils.StringToNullString(complaintInfo.OppositeName) if complaintInfo.ComplainAmount != "" { amount, err := s.parseDecimal(complaintInfo.ComplainAmount) if err == nil { complaintRecord.ComplainAmount = lzUtils.Float64ToNullFloat64(amount) } } complaintRecord.GmtComplain = s.parseTime(complaintInfo.GmtComplain) complaintRecord.GmtProcess = s.parseTime(complaintInfo.GmtProcess) complaintRecord.GmtOverdue = s.parseTime(complaintInfo.GmtOverdue) complaintRecord.ComplainContent = lzUtils.StringToNullString(complaintInfo.ComplainContent) complaintRecord.TradeNo = lzUtils.StringToNullString(complaintInfo.TradeNo) complaintRecord.Status = lzUtils.StringToNullString(complaintInfo.Status) complaintRecord.StatusDescription = lzUtils.StringToNullString(complaintInfo.StatusDescription) complaintRecord.ProcessCode = lzUtils.StringToNullString(complaintInfo.ProcessCode) complaintRecord.ProcessMessage = lzUtils.StringToNullString(complaintInfo.ProcessMessage) complaintRecord.ProcessRemark = lzUtils.StringToNullString(complaintInfo.ProcessRemark) complaintRecord.GmtRiskFinishTime = s.parseTime(complaintInfo.GmtRiskFinishTime) complaintRecord.ComplainUrl = lzUtils.StringToNullString(complaintInfo.ComplainUrl) // 处理图片列表(转换为JSON字符串) if len(complaintInfo.ProcessImgUrlList) > 0 { imgJson, _ := json.Marshal(complaintInfo.ProcessImgUrlList) complaintRecord.ProcessImgUrlList = lzUtils.StringToNullString(string(imgJson)) } if len(complaintInfo.CertifyInfo) > 0 { certifyJson, _ := json.Marshal(complaintInfo.CertifyInfo) complaintRecord.CertifyInfo = lzUtils.StringToNullString(string(certifyJson)) } // 保存或更新支付宝投诉表 if isUpdate { if err := s.ComplaintAlipayModel.UpdateWithVersion(transCtx, session, complaintRecord); err != nil { return errors.Wrapf(err, "更新支付宝投诉表失败, task_id: %s", complaintInfo.TaskId) } } else { if _, err := s.ComplaintAlipayModel.Insert(transCtx, session, complaintRecord); err != nil { return errors.Wrapf(err, "插入支付宝投诉表失败, task_id: %s", complaintInfo.TaskId) } } // 3. 保存交易信息 if len(complaintInfo.ComplaintTradeInfoList) > 0 { // 先删除旧的交易信息(如果存在) if isUpdate { oldTrades, _ := s.ComplaintAlipayTradeModel.FindAll(transCtx, s.ComplaintAlipayTradeModel.SelectBuilder(). Where("complaint_id = ? AND del_state = ?", complaintRecord.Id, 0), "") for _, oldTrade := range oldTrades { oldTrade.DelState = 1 oldTrade.DeleteTime = lzUtils.TimeToNullTime(time.Now()) s.ComplaintAlipayTradeModel.UpdateWithVersion(transCtx, session, oldTrade) } } // 插入新的交易信息 for _, tradeInfo := range complaintInfo.ComplaintTradeInfoList { trade := &model.ComplaintAlipayTrade{ Id: uuid.NewString(), ComplaintId: complaintRecord.Id, AlipayTradeId: lzUtils.Int64ToNullInt64(tradeInfo.Id), AlipayComplaintRecordId: lzUtils.Int64ToNullInt64(tradeInfo.ComplaintRecordId), TradeNo: lzUtils.StringToNullString(tradeInfo.TradeNo), OutNo: lzUtils.StringToNullString(tradeInfo.OutNo), GmtTrade: s.parseTime(tradeInfo.GmtTrade), GmtRefund: s.parseTime(tradeInfo.GmtRefund), Status: lzUtils.StringToNullString(tradeInfo.Status), StatusDescription: lzUtils.StringToNullString(tradeInfo.StatusDescription), } if tradeInfo.Amount != "" { amount, err := s.parseDecimal(tradeInfo.Amount) if err == nil { trade.Amount = lzUtils.Float64ToNullFloat64(amount) } } if _, err := s.ComplaintAlipayTradeModel.Insert(transCtx, session, trade); err != nil { return errors.Wrapf(err, "插入投诉交易信息失败, task_id: %s", complaintInfo.TaskId) } } } return nil }) } // GetLatestComplainTime 查询数据库中最新投诉的投诉时间 func (s *AlipayComplaintService) GetLatestComplainTime(ctx context.Context) (time.Time, error) { // 从支付宝投诉表查询最新投诉时间(因为主表没有 gmt_complain 字段) builder := s.ComplaintAlipayModel.SelectBuilder(). Where("del_state = ?", 0). OrderBy("gmt_complain DESC"). Limit(1) complaints, err := s.ComplaintAlipayModel.FindAll(ctx, builder, "") if err != nil { return time.Time{}, errors.Wrapf(err, "查询最新投诉时间失败") } if len(complaints) == 0 { return time.Time{}, nil // 数据库为空 } // 返回最新投诉的投诉时间 if complaints[0].GmtComplain.Valid { return complaints[0].GmtComplain.Time, nil } return time.Time{}, nil } // parseTime 解析时间字符串 func (s *AlipayComplaintService) parseTime(timeStr string) sql.NullTime { if timeStr == "" { return sql.NullTime{Valid: false} } // 尝试多种时间格式 formats := []string{ "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02", } for _, format := range formats { if t, err := time.Parse(format, timeStr); err == nil { return sql.NullTime{Time: t, Valid: true} } } return sql.NullTime{Valid: false} } // parseDecimal 解析金额字符串 func (s *AlipayComplaintService) parseDecimal(amountStr string) (float64, error) { if amountStr == "" { return 0, nil } // 使用 fmt.Sscanf 解析 var amount float64 _, err := fmt.Sscanf(amountStr, "%f", &amount) return amount, err } // findOrderByPlatformOrderId 根据支付宝交易单号查找订单ID func (s *AlipayComplaintService) findOrderByPlatformOrderId(ctx context.Context, platformOrderId string) string { if platformOrderId == "" { return "" } // 通过 PlatformOrderId 查找订单 builder := s.OrderModel.SelectBuilder(). Where("platform_order_id = ? AND del_state = ?", platformOrderId, 0). Limit(1) orders, err := s.OrderModel.FindAll(ctx, builder, "") if err != nil || len(orders) == 0 { return "" } return orders[0].Id }