chore: 并入 iotlcm-app 历史到 app/(原提交 f40bda9)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:00 +08:00
1107 changed files with 147143 additions and 0 deletions
@@ -0,0 +1,546 @@
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
}