Files
ycc-proxy-server/app/main/api/internal/service/alipayComplaintService.go
2026-01-25 19:07:24 +08:00

540 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.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.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
}