package app import ( "bytes" "context" "fmt" "os" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" "git.5iot.com/iotlcm-public/iotlcm-app/utility/hash" "github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "github.com/xuri/excelize/v2" ) type appRenewBatchLogFiles struct { ImportOssId int64 ImportFileName string ResultOssId int64 DownloadId int64 } func (s *sAppCard) ensureAppRenewBatchLogFiles(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, existingInfo *gjson.Json, batchNo string, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, buyerNote string, now *gtime.Time) (*appRenewBatchLogFiles, error) { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return nil, gerror.New("续费批次号不能为空") } if user == nil { return nil, gerror.New("当前用户不存在") } if batch == nil || batch.Party == nil { return nil, gerror.New("续费批次日志文件信息不完整") } files := appRenewBatchLogFilesFromInfo(existingInfo) existingDownload, err := appRenewBatchResultDownload(ctx, tx, batchNo) if err != nil { return nil, err } if existingDownload != nil { files.ResultOssId = existingDownload.ResultOssId files.DownloadId = existingDownload.DownloadId } if files.ImportOssId <= 0 { importContent, err := appBuildRenewBatchImportExcel(batch, sourceBatchNo, durationMonths, buyerNote, now) if err != nil { return nil, gerror.Wrap(err, "生成续费批次源文件失败") } importFileName := appRenewBatchImportFileNameByBatchNo(batchNo) importUpload, importOssId, err := appSaveRenewBatchOss(ctx, tx, user, batch.Party.CreatedDept, importContent, importFileName, consts.SimCardRenewSubDirImportExcel, now) if err != nil { return nil, gerror.Wrap(err, "保存续费批次源文件失败") } files.ImportOssId = importOssId files.ImportFileName = strings.TrimSpace(importUpload.Name) } if strings.TrimSpace(files.ImportFileName) == "" { files.ImportFileName = appRenewBatchImportFileName(ctx, tx, files.ImportOssId, appRenewBatchImportFileNameByBatchNo(batchNo)) } if existingDownload != nil { return files, nil } resultContent, err := appBuildRenewBatchResultExcel(batch, files, batchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, now, user.UserId, 0) if err != nil { return nil, gerror.Wrap(err, "生成续费批次结果文件失败") } resultUpload, resultOssId, err := appSaveRenewBatchOss(ctx, tx, user, batch.Party.CreatedDept, resultContent, batchNo+"-result.xlsx", consts.SimCardRenewSubDirResultExcel, now) if err != nil { return nil, gerror.Wrap(err, "保存续费批次结果文件失败") } files.ResultOssId = resultOssId if err = appFillRenewResultOssId(ctx, tx, resultOssId, resultUpload.Path, user.UserId, now); err != nil { return nil, err } refreshedResult, err := appLoadRenewOssUpload(ctx, tx, resultOssId, resultUpload) if err != nil { return nil, err } quantity := len(batch.Items) downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).Data(do.TResourceBatchDownload{ BatchNo: batchNo, TaskName: consts.ResourceBatchDownloadTaskSimCardRenew, ResourceType: consts.ResourceTypeExcel, ExportScene: consts.ResourceExportSceneRenew, TotalCount: quantity, SuccessCount: quantity, FailCount: 0, DownloadStatus: consts.DownloadStatusGenerated, OssId: resultOssId, FileName: refreshedResult.Name, FileExt: refreshedResult.Ext, FileUrl: refreshedResult.FullPath, FilePath: refreshedResult.Path, FileSize: refreshedResult.Size, FileMd5: refreshedResult.Md5, DownloadCount: 0, MaxDownloadCount: 0, DeptId: batch.Party.CreatedDept, StartTime: now, FinishTime: now, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).InsertAndGetId() if err != nil { return nil, gerror.Wrap(err, "写入续费批次结果下载记录失败") } files.DownloadId = downloadId return files, nil } func appRenewBatchLogFilesFromInfo(info *gjson.Json) *appRenewBatchLogFiles { files := &appRenewBatchLogFiles{} if info == nil { return files } files.ImportOssId = info.Get("importFileOssId").Int64() files.ResultOssId = info.Get("resultFileOssId").Int64() files.DownloadId = info.Get("resultDownloadId").Int64() return files } func appRenewBatchResultDownload(ctx context.Context, tx gdb.TX, batchNo string) (*appRenewBatchLogFiles, error) { var downloads []*entity.TResourceBatchDownload columns := dao.TResourceBatchDownload.Columns() err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx). Fields(columns.Id, columns.OssId). Where(columns.BatchNo, strings.TrimSpace(batchNo)). OrderDesc(columns.Id). Limit(1). Scan(&downloads) if err != nil { return nil, gerror.Wrap(err, "查询续费批次结果下载记录失败") } if len(downloads) == 0 || downloads[0] == nil || downloads[0].Id == 0 { return nil, nil } return &appRenewBatchLogFiles{ DownloadId: downloads[0].Id, ResultOssId: downloads[0].OssId, }, nil } func appRenewBatchInfoWithLogFiles(info *gjson.Json, files *appRenewBatchLogFiles) *gjson.Json { data := map[string]any{} if info != nil { for key, value := range info.Map() { data[key] = value } } if files != nil { if files.ImportOssId > 0 { data["importFileOssId"] = files.ImportOssId } if files.ResultOssId > 0 { data["resultFileOssId"] = files.ResultOssId } if files.DownloadId > 0 { data["resultDownloadId"] = files.DownloadId } } return gjson.New(data) } func appSaveRenewBatchOss(ctx context.Context, tx gdb.TX, user *entity.SysUser, deptId int64, content []byte, fileName string, subDirName string, now *gtime.Time) (model.UploadResponse, int64, error) { upload, err := service.SysOss().SaveContentLocal(ctx, content, consts.SysUploadNewFileTypeUserFile, fileName, subDirName, 0) if err != nil { return upload, 0, err } ossId, err := dao.SysOss.Ctx(ctx).TX(tx).Data(do.SysOss{ TenantId: user.TenantId, FileName: upload.Name, OriginalName: upload.OriginalName, FileSuffix: upload.Ext, Path: upload.Path, Url: upload.FullPath, CreatedDept: deptId, CreatedAt: now, CreatedBy: user.UserId, UpdatedAt: now, UpdatedBy: user.UserId, Service: "local", Md5: upload.Md5, FileSize: upload.Size, FileCrc16: upload.Crc16, FileSum: upload.Sum16, }).InsertAndGetId() if err != nil { return upload, 0, gerror.Wrap(err, "写入续费批次OSS记录失败") } return upload, ossId, nil } func appRenewBatchImportHeaders() []string { return []string{ "ICCID", "MSISDN", "IMSI", "续费年限", "是否走线下", } } func appBuildRenewBatchImportExcel(batch *appRenewBatchContext, sourceBatchNo string, durationMonths int, buyerNote string, now *gtime.Time) ([]byte, error) { f := excelize.NewFile() defer f.Close() sheet := "Sheet1" headers := appRenewBatchImportHeaders() if err := f.SetSheetRow(sheet, "A1", &headers); err != nil { return nil, err } // 源文件保持管理平台续费导入模板格式,方便批次日志统一下载和排查。 if err := appApplyHeaderColumnsTextStyle(f, sheet); err != nil { return nil, err } durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations) for index, item := range batch.Items { row := []interface{}{ strings.TrimSpace(item.Card.Iccid), strings.TrimSpace(item.Card.Msisdn), strings.TrimSpace(item.Card.Imsi), durationYears, true, } cell, err := excelize.CoordinatesToCellName(1, index+2) if err != nil { return nil, err } if err = f.SetSheetRow(sheet, cell, &row); err != nil { return nil, err } } _ = f.SetColWidth(sheet, "A", "A", 24) _ = f.SetColWidth(sheet, "B", "B", 14) _ = f.SetColWidth(sheet, "C", "C", 18) _ = f.SetColWidth(sheet, "D", "D", 18) _ = f.SetColWidth(sheet, "E", "E", 18) return appExcelBytes(f) } func appBuildRenewBatchResultExcel(batch *appRenewBatchContext, files *appRenewBatchLogFiles, batchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, now *gtime.Time, userId int64, resultOssId int64) ([]byte, error) { f := excelize.NewFile() defer f.Close() leftStyleID, err := f.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "left", Vertical: "center", }, }) if err != nil { return nil, err } resultSheet := "导入结果" if err = f.SetSheetName("Sheet1", resultSheet); err != nil { return nil, err } headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"} if err = f.SetSheetRow(resultSheet, "A1", &headers); err != nil { return nil, err } for index, item := range batch.Items { row := []interface{}{ appRenewBatchSourceRowNo(index), strings.TrimSpace(item.Card.Iccid), "成功", "", } cell, err := excelize.CoordinatesToCellName(1, index+2) if err != nil { return nil, err } if err = f.SetSheetRow(resultSheet, cell, &row); err != nil { return nil, err } } resultEndCell, err := excelize.CoordinatesToCellName(4, len(batch.Items)+1) if err != nil { return nil, err } if err = f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil { return nil, err } infoSheet := "批次信息" if _, err = f.NewSheet(infoSheet); err != nil { return nil, err } infoHeaders := []string{"字段", "值"} if err = f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil { return nil, err } infoRows := appRenewBatchResultInfoRows(batch, files, batchNo, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, now, userId, resultOssId) for index, item := range infoRows { row := []interface{}{item[0], item[1]} cell, err := excelize.CoordinatesToCellName(1, index+2) if err != nil { return nil, err } if err = f.SetSheetRow(infoSheet, cell, &row); err != nil { return nil, err } } infoEndCell, err := excelize.CoordinatesToCellName(2, len(infoRows)+1) if err != nil { return nil, err } if err = f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil { return nil, err } _ = f.SetColWidth(resultSheet, "A", "A", 14) _ = f.SetColWidth(resultSheet, "B", "B", 24) _ = f.SetColWidth(resultSheet, "C", "C", 12) _ = f.SetColWidth(resultSheet, "D", "D", 40) _ = f.SetColWidth(infoSheet, "A", "B", 24) f.SetActiveSheet(0) return appExcelBytes(f) } func appApplyHeaderColumnsTextStyle(f *excelize.File, sheetName string) error { if f == nil { return gerror.New("Excel文件不能为空") } sheetName = strings.TrimSpace(sheetName) if sheetName == "" { sheetName = f.GetSheetName(f.GetActiveSheetIndex()) } if sheetName == "" { sheets := f.GetSheetList() if len(sheets) == 0 { return gerror.New("Excel文件没有工作表") } sheetName = sheets[0] } rows, err := f.GetRows(sheetName) if err != nil { return err } if len(rows) == 0 || len(rows[0]) == 0 { return gerror.New("Excel文件表头为空") } styleID, err := f.NewStyle(&excelize.Style{NumFmt: 49}) if err != nil { return err } for index, header := range rows[0] { if strings.TrimSpace(header) == "" { continue } colName, err := excelize.ColumnNumberToName(index + 1) if err != nil { return err } if err = f.SetColStyle(sheetName, colName, styleID); err != nil { return err } } return nil } func appRenewBatchResultInfoRows(batch *appRenewBatchContext, files *appRenewBatchLogFiles, batchNo string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, now *gtime.Time, userId int64, resultOssId int64) [][2]string { importFileName := "" importOssId := "" if files != nil { importFileName = strings.TrimSpace(files.ImportFileName) if files.ImportOssId > 0 { importOssId = fmt.Sprintf("%d", files.ImportOssId) } } if importFileName == "" { importFileName = appRenewBatchImportFileNameByBatchNo(batchNo) } resultFileOssId := "" if resultOssId > 0 { resultFileOssId = fmt.Sprintf("%d", resultOssId) } return [][2]string{ {"批次号", strings.TrimSpace(batchNo)}, {"批次类型", "卡续费"}, {"处理结果", "成功"}, {"总行数", fmt.Sprintf("%d", len(batch.Items))}, {"校验通过行数", fmt.Sprintf("%d", len(batch.Items))}, {"失败行数", "0"}, {"实际写入行数", fmt.Sprintf("%d", len(batch.Items))}, {"原始文件名", importFileName}, {"原始文件OSS ID", importOssId}, {"结果文件OSS ID", resultFileOssId}, {"操作人", fmt.Sprintf("%d", userId)}, {"开始时间", appRenewFormatTime(now)}, {"结束时间", appRenewFormatTime(now)}, {"备注", "导入成功"}, } } func appFillRenewResultOssId(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error { if ossId <= 0 || strings.TrimSpace(filePath) == "" { return gerror.New("续费结果文件OSS信息不完整") } f, err := excelize.OpenFile(filePath) if err != nil { return gerror.Wrap(err, "打开续费结果文件失败") } defer f.Close() rows, err := f.GetRows("批次信息") if err != nil { return gerror.Wrap(err, "读取续费结果批次信息失败") } found := false for index, row := range rows { if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" { continue } cell, err := excelize.CoordinatesToCellName(2, index+1) if err != nil { return err } if err = f.SetCellStr("批次信息", cell, fmt.Sprintf("%d", ossId)); err != nil { return err } found = true break } if !found { return gerror.New("续费结果文件缺少结果文件OSS ID字段") } if err = f.SaveAs(filePath); err != nil { return gerror.Wrap(err, "回写续费结果文件OSS ID失败") } return appRefreshRenewOssFileInfo(ctx, tx, ossId, filePath, userId, now) } func appRefreshRenewOssFileInfo(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error { fileInfo, err := os.Stat(filePath) if err != nil { return gerror.Wrap(err, "读取续费结果文件信息失败") } md5, _ := gmd5.EncryptFile(filePath) crc16 := hash.Crc16ChecksumFile(filePath) sum16 := hash.Sum16ChecksumFile(filePath) _, err = dao.SysOss.Ctx(ctx).TX(tx). Where(dao.SysOss.Columns().OssId, ossId). Data(do.SysOss{ Md5: md5, FileSize: fileInfo.Size(), FileCrc16: crc16, FileSum: sum16, UpdatedAt: now, UpdatedBy: userId, }). Update() if err != nil { return gerror.Wrap(err, "更新续费结果文件信息失败") } return nil } func appLoadRenewOssUpload(ctx context.Context, tx gdb.TX, ossId int64, fallback model.UploadResponse) (model.UploadResponse, error) { if ossId <= 0 { return fallback, nil } var oss *entity.SysOss err := dao.SysOss.Ctx(ctx).TX(tx). Where(dao.SysOss.Columns().OssId, ossId). Limit(1). Scan(&oss) if err != nil { return fallback, gerror.Wrap(err, "读取续费结果文件OSS记录失败") } if oss == nil || oss.OssId == 0 { return fallback, nil } fallback.Name = oss.FileName fallback.OriginalName = oss.OriginalName fallback.Ext = oss.FileSuffix fallback.Path = oss.Path fallback.FullPath = oss.Url fallback.Size = int64(oss.FileSize) fallback.Md5 = oss.Md5 return fallback, nil } func appRenewBatchImportFileName(ctx context.Context, tx gdb.TX, ossId int64, sourceName string) string { if ossId <= 0 { sourceName = strings.TrimSpace(sourceName) if sourceName != "" { return sourceName } return "input-prepare" } var oss *entity.SysOss if err := dao.SysOss.Ctx(ctx).TX(tx). Fields(dao.SysOss.Columns().FileName). Where(dao.SysOss.Columns().OssId, ossId). Limit(1). Scan(&oss); err != nil { return strings.TrimSpace(sourceName) } if oss != nil && strings.TrimSpace(oss.FileName) != "" { return strings.TrimSpace(oss.FileName) } return strings.TrimSpace(sourceName) } func appRenewBatchImportFileNameByBatchNo(batchNo string) string { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return "input-prepare" } return batchNo + "-import.xlsx" } func appRenewBatchRenewDate(now *gtime.Time) string { if now == nil { return "" } return now.Format("Y-m-d") + " 00:00:00" } func appExcelBytes(f *excelize.File) ([]byte, error) { buffer := bytes.NewBuffer(nil) if err := f.Write(buffer); err != nil { return nil, err } return buffer.Bytes(), nil }