This commit is contained in:
2026-01-12 16:43:08 +08:00
parent dc747139c9
commit 3c6e2683f5
110 changed files with 9630 additions and 481 deletions

View File

@@ -0,0 +1,541 @@
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
}