chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
package batchresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
const configOssURLPathKey = "sys.oss.urlPath"
|
||||
|
||||
type Row struct {
|
||||
RowNo int
|
||||
ObjectCode string
|
||||
TargetCode string
|
||||
PackageCode string
|
||||
EffectiveMode string
|
||||
ICCID string
|
||||
MSISDN string
|
||||
IMSI string
|
||||
TargetDeptName string
|
||||
StatusBefore int
|
||||
TargetStatus int
|
||||
Operation string
|
||||
ApnName string
|
||||
DetailStatus int
|
||||
Success bool
|
||||
Status string
|
||||
Reason string
|
||||
UpstreamCode int
|
||||
OrderNum string
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
TenantID string
|
||||
Rows []Row
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
FileName string
|
||||
OriginalName string
|
||||
FileSuffix string
|
||||
Path string
|
||||
LocalPath string
|
||||
URL string
|
||||
Size int64
|
||||
MD5 string
|
||||
}
|
||||
|
||||
type Generator interface {
|
||||
Create(ctx context.Context, request Request) (Artifact, error)
|
||||
}
|
||||
|
||||
type generator struct{}
|
||||
|
||||
func New() Generator {
|
||||
return &generator{}
|
||||
}
|
||||
|
||||
func (gtr *generator) Create(ctx context.Context, request Request) (Artifact, error) {
|
||||
if request.BatchNo == "" || filepath.Base(request.BatchNo) != request.BatchNo {
|
||||
return Artifact{}, gerror.New("invalid batch number")
|
||||
}
|
||||
uploadPath := g.Cfg().MustGet(ctx, "system.upload.local.path").String()
|
||||
if uploadPath == "" {
|
||||
return Artifact{}, gerror.New("system.upload.local.path is empty")
|
||||
}
|
||||
tenantID := request.TenantID
|
||||
if tenantID == "" {
|
||||
tenantID = "default"
|
||||
}
|
||||
resultDir, err := resultDir(request.BatchType)
|
||||
if err != nil {
|
||||
return Artifact{}, err
|
||||
}
|
||||
objectDir := filepath.Join(tenantID, "user", "file", time.Now().Format("20060102"), resultDir)
|
||||
physicalUploadPath := resultPhysicalUploadRoot(uploadPath)
|
||||
directory := filepath.Join(physicalUploadPath, objectDir)
|
||||
if err = os.MkdirAll(directory, 0o755); err != nil {
|
||||
return Artifact{}, gerror.Wrap(err, "create batch result directory")
|
||||
}
|
||||
originalName := request.BatchNo + "-result.xlsx"
|
||||
var nonce [16]byte
|
||||
if _, err = rand.Read(nonce[:]); err != nil {
|
||||
return Artifact{}, gerror.Wrap(err, "generate batch result file name")
|
||||
}
|
||||
fileName := request.BatchNo + "-result-" + hex.EncodeToString(nonce[:]) + ".xlsx"
|
||||
filePath := filepath.Join(directory, fileName)
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
return Artifact{}, gerror.Wrap(err, "reserve batch result file")
|
||||
}
|
||||
if err = file.Close(); err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return Artifact{}, gerror.Wrap(err, "close reserved result file")
|
||||
}
|
||||
logicalPath := filepath.Join(uploadPath, objectDir, fileName)
|
||||
if err = writeWorkbook(filePath, request); err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return Artifact{}, err
|
||||
}
|
||||
artifact, err := inspectArtifact(ctx, filePath, logicalPath, fileName, originalName, tenantID)
|
||||
if err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return Artifact{}, err
|
||||
}
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func resultPhysicalUploadRoot(uploadPath string) string {
|
||||
uploadPath = strings.TrimSpace(uploadPath)
|
||||
for _, envName := range []string{"IOTLCM_WORKER_UPLOAD_ROOT", "IOTLCM_UPLOAD_ROOT"} {
|
||||
if value := strings.TrimSpace(os.Getenv(envName)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
for _, candidate := range uploadRootEquivalentCandidates(uploadPath) {
|
||||
if samePath(candidate, uploadPath) {
|
||||
continue
|
||||
}
|
||||
for _, resolved := range resolvedRootCandidates(candidate) {
|
||||
if info, err := os.Stat(resolved); err == nil && info.IsDir() {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
func uploadRootEquivalentCandidates(root string) []string {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return nil
|
||||
}
|
||||
candidates := []string{root}
|
||||
normalized := filepath.ToSlash(filepath.Clean(root))
|
||||
if normalized == "resource/upload" || strings.HasSuffix(normalized, "/resource/upload") {
|
||||
prefix := strings.TrimSuffix(normalized, "resource/upload")
|
||||
candidates = appendUniqueString(candidates, filepath.FromSlash(prefix+"upload"))
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func resolvedRootCandidates(root string) []string {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return nil
|
||||
}
|
||||
if filepath.IsAbs(root) {
|
||||
return []string{root}
|
||||
}
|
||||
candidates := []string{root}
|
||||
for _, base := range localBaseDirs() {
|
||||
candidates = appendUniqueString(candidates, filepath.Join(base, root))
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func localBaseDirs() []string {
|
||||
var dirs []string
|
||||
if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" {
|
||||
dirs = appendUniqueString(dirs, wd)
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil && strings.TrimSpace(exe) != "" {
|
||||
dirs = appendUniqueString(dirs, filepath.Dir(exe))
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func samePath(a, b string) bool {
|
||||
return filepath.Clean(strings.TrimSpace(a)) == filepath.Clean(strings.TrimSpace(b))
|
||||
}
|
||||
|
||||
func appendUniqueString(values []string, value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return values
|
||||
}
|
||||
for _, existing := range values {
|
||||
if samePath(existing, value) {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
|
||||
func resultDir(batchType int) (string, error) {
|
||||
if batchType == 11 {
|
||||
return filepath.Join("sim-card-add-package", "result"), nil
|
||||
}
|
||||
if batchType == 12 {
|
||||
return filepath.Join("sim-pool-add-package", "result"), nil
|
||||
}
|
||||
switch batchType {
|
||||
case 1:
|
||||
return filepath.Join("sim-card-inbound", "result"), nil
|
||||
case 3:
|
||||
return filepath.Join("sim-card-allocate", "result"), nil
|
||||
case 4:
|
||||
return filepath.Join("sim-card-renew-change-prepare", "result"), nil
|
||||
case 101:
|
||||
return filepath.Join("sim-card-renew", "result"), nil
|
||||
case 102:
|
||||
return filepath.Join("sim-card-change", "result"), nil
|
||||
case 103:
|
||||
return filepath.Join("sim-card-start", "result"), nil
|
||||
case 104:
|
||||
return filepath.Join("sim-card-stop", "result"), nil
|
||||
case 105:
|
||||
return filepath.Join("sim-card-restart", "result"), nil
|
||||
case 106:
|
||||
return filepath.Join("sim-card-cancel", "result"), nil
|
||||
case 107:
|
||||
return filepath.Join("sim-card-data-cut", "result"), nil
|
||||
case 108:
|
||||
return filepath.Join("sim-card-unsub", "result"), nil
|
||||
case 109:
|
||||
return filepath.Join("sim-card-data-start", "result"), nil
|
||||
case 110:
|
||||
return filepath.Join("sim-card-data-stop", "result"), nil
|
||||
case 40:
|
||||
return filepath.Join("sim-warning-config", "result"), nil
|
||||
case 41:
|
||||
return filepath.Join("sim-card-traffic-cutoff-config", "result"), nil
|
||||
default:
|
||||
return "", gerror.Newf("unsupported batch type %d", batchType)
|
||||
}
|
||||
}
|
||||
|
||||
func writeWorkbook(path string, request Request) error {
|
||||
file := excelize.NewFile()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
const resultSheet = "处理结果"
|
||||
defaultSheet := file.GetSheetName(0)
|
||||
if err := file.SetSheetName(defaultSheet, resultSheet); err != nil {
|
||||
return gerror.Wrap(err, "name result sheet")
|
||||
}
|
||||
if request.BatchType == 1 {
|
||||
return writeInboundWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
if request.BatchType == 3 {
|
||||
return writeAllocateWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
if request.BatchType == 11 || request.BatchType == 12 {
|
||||
return writeAddPackageWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
if request.BatchType == 106 {
|
||||
return writeCancelWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
if request.BatchType == 40 || request.BatchType == 41 {
|
||||
return writeWarningConfigWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
if isDataServiceBatch(request.BatchType) {
|
||||
return writeDataServiceWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
return writeLifecycleWorkbook(file, resultSheet, path, request)
|
||||
}
|
||||
|
||||
func writeAllocateWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "ICCID", "MSISDN", "IMSI", "目标客户", "执行结果", "失败原因"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write allocate result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
if row.Status != "" {
|
||||
status = row.Status
|
||||
}
|
||||
values := []interface{}{row.RowNo, row.ICCID, row.MSISDN, row.IMSI, row.TargetDeptName, status, row.Reason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate allocate result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write allocate result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format allocate result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "E", 24); err != nil {
|
||||
return gerror.Wrap(err, "format allocate result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "F", "G", 24); err != nil {
|
||||
return gerror.Wrap(err, "format allocate result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save allocate result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeWarningConfigWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "对象编码", "执行结果", "失败原因"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write warning config result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
objectCode := row.ObjectCode
|
||||
if objectCode == "" {
|
||||
objectCode = row.ICCID
|
||||
}
|
||||
values := []interface{}{row.RowNo, objectCode, status, row.Reason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate warning config result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write warning config result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format warning config result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "B", 28); err != nil {
|
||||
return gerror.Wrap(err, "format warning config result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "C", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format warning config result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save warning config result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAddPackageWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "目标编码", "加油包编码", "生效方式", "执行结果", "失败原因", "订单号"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write add package result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
targetCode := row.ObjectCode
|
||||
if targetCode == "" {
|
||||
targetCode = row.TargetCode
|
||||
}
|
||||
values := []interface{}{row.RowNo, targetCode, row.PackageCode, addPackageEffectiveModeText(row.EffectiveMode), status, row.Reason, row.OrderNum}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate add package result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write add package result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format add package result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format add package result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "E", "G", 22); err != nil {
|
||||
return gerror.Wrap(err, "format add package result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save add package result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addPackageEffectiveModeText(mode string) string {
|
||||
switch strings.TrimSpace(mode) {
|
||||
case "", "current_month", "immediate", "this_month", "本月生效", "本月", "当月":
|
||||
return "本月生效"
|
||||
case "next_month", "nextMonth", "次月生效", "次月", "下月", "下月生效":
|
||||
return "次月生效"
|
||||
default:
|
||||
return strings.TrimSpace(mode)
|
||||
}
|
||||
}
|
||||
|
||||
func isDataServiceBatch(batchType int) bool {
|
||||
return batchType == 107 || batchType == 109 || batchType == 110
|
||||
}
|
||||
|
||||
func writeInboundWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "ICCID", "执行结果", "失败原因"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write inbound result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
if row.Status != "" {
|
||||
status = row.Status
|
||||
}
|
||||
values := []interface{}{row.RowNo, row.ICCID, status, row.Reason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate inbound result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write inbound result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format inbound result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "B", 24); err != nil {
|
||||
return gerror.Wrap(err, "format inbound result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "C", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format inbound result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save inbound result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeLifecycleWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "ICCID", "MSISDN", "IMSI", "执行结果", "失败原因", "上游状态码", "上游订单号"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write lifecycle result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
values := []interface{}{row.RowNo, row.ICCID, row.MSISDN, row.IMSI, status, row.Reason, row.UpstreamCode, row.OrderNum}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate lifecycle result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write lifecycle result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format lifecycle result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format lifecycle result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "E", "H", 20); err != nil {
|
||||
return gerror.Wrap(err, "format lifecycle result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save lifecycle result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCancelWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "ICCID", "MSISDN", "IMSI", "原卡状态", "目标卡状态", "执行结果", "失败原因"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write cancel result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
values := []interface{}{row.RowNo, row.ICCID, row.MSISDN, row.IMSI, row.StatusBefore, row.TargetStatus, status, row.Reason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate cancel result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write cancel result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format cancel result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format cancel result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "E", "H", 20); err != nil {
|
||||
return gerror.Wrap(err, "format cancel result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save cancel result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDataServiceWorkbook(file *excelize.File, sheet, path string, request Request) error {
|
||||
headers := []interface{}{"行号", "ICCID", "MSISDN", "IMSI", "操作", "APN", "执行结果", "失败/待核查原因", "上游状态码", "上游订单号"}
|
||||
if err := file.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return gerror.Wrap(err, "write data service result headers")
|
||||
}
|
||||
for index, row := range request.Rows {
|
||||
status := "失败"
|
||||
if row.DetailStatus == consts.BatchDetailStatusUnknown {
|
||||
status = "待核查"
|
||||
} else if row.Success {
|
||||
status = "成功"
|
||||
}
|
||||
values := []interface{}{row.RowNo, row.ICCID, row.MSISDN, row.IMSI, row.Operation, row.ApnName, status, row.Reason, row.UpstreamCode, row.OrderNum}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "locate data service result row")
|
||||
}
|
||||
if err = file.SetSheetRow(sheet, cell, &values); err != nil {
|
||||
return gerror.Wrap(err, "write data service result row")
|
||||
}
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "A", "A", 10); err != nil {
|
||||
return gerror.Wrap(err, "format data service result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "B", "D", 24); err != nil {
|
||||
return gerror.Wrap(err, "format data service result workbook")
|
||||
}
|
||||
if err := file.SetColWidth(sheet, "E", "J", 20); err != nil {
|
||||
return gerror.Wrap(err, "format data service result workbook")
|
||||
}
|
||||
if err := file.SaveAs(path); err != nil {
|
||||
return gerror.Wrap(err, "save data service result workbook")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inspectArtifact(ctx context.Context, localPath, logicalPath, fileName, originalName, tenantID string) (Artifact, error) {
|
||||
file, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return Artifact{}, gerror.Wrap(err, "open result file")
|
||||
}
|
||||
defer file.Close()
|
||||
hash := md5.New()
|
||||
size, err := io.Copy(hash, file)
|
||||
if err != nil {
|
||||
return Artifact{}, gerror.Wrap(err, "hash result file")
|
||||
}
|
||||
return Artifact{
|
||||
FileName: fileName,
|
||||
OriginalName: originalName,
|
||||
FileSuffix: "xlsx",
|
||||
Path: logicalPath,
|
||||
LocalPath: localPath,
|
||||
URL: localUploadURL(ctx, logicalPath, tenantID),
|
||||
Size: size,
|
||||
MD5: hex.EncodeToString(hash.Sum(nil)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RemoveLocalFile(artifact Artifact) {
|
||||
path := strings.TrimSpace(artifact.LocalPath)
|
||||
if path == "" {
|
||||
path = strings.TrimSpace(artifact.Path)
|
||||
}
|
||||
if path != "" {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
func localUploadURL(ctx context.Context, filePath string, tenantID string) string {
|
||||
filePath = strings.TrimSpace(filepath.ToSlash(filepath.Clean(filePath)))
|
||||
if filePath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if configValue := loadUploadURLPathConfig(ctx, tenantID); configValue != "" {
|
||||
if strings.Contains(configValue, "|") {
|
||||
split := strings.SplitN(configValue, "|", 2)
|
||||
from := strings.TrimSpace(filepath.ToSlash(split[0]))
|
||||
to := strings.TrimSpace(filepath.ToSlash(split[1]))
|
||||
if suffix, ok := localUploadMappedSuffix(filePath, from); ok {
|
||||
return joinUploadURL(to, suffix)
|
||||
}
|
||||
} else {
|
||||
if suffix := localUploadRelativePath(filePath); suffix != "" {
|
||||
return joinUploadURL(configValue, suffix)
|
||||
}
|
||||
return joinUploadURL(configValue, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if suffix := localUploadRelativePath(filePath); suffix != "" {
|
||||
return joinUploadURL("/upload", suffix)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
func loadUploadURLPathConfig(ctx context.Context, tenantID string) (configValue string) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
configValue = ""
|
||||
}
|
||||
}()
|
||||
for _, tenant := range []string{strings.TrimSpace(tenantID), "000000"} {
|
||||
if tenant == "" {
|
||||
continue
|
||||
}
|
||||
var row struct {
|
||||
ConfigValue string `orm:"config_value"`
|
||||
}
|
||||
err := g.DB().Model("sys_config").Safe().Ctx(ctx).
|
||||
Fields("config_value").
|
||||
Where("tenant_id", tenant).
|
||||
Where("config_key", configOssURLPathKey).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err == nil && strings.TrimSpace(row.ConfigValue) != "" {
|
||||
return strings.TrimSpace(row.ConfigValue)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func localUploadMappedSuffix(filePath string, from string) (string, bool) {
|
||||
from = strings.Trim(strings.TrimSpace(filepath.ToSlash(from)), "/")
|
||||
if from == "" {
|
||||
return "", false
|
||||
}
|
||||
normalized := strings.TrimSpace(filepath.ToSlash(filePath))
|
||||
marker := "/" + from + "/"
|
||||
if idx := strings.Index(normalized, marker); idx >= 0 {
|
||||
return strings.TrimPrefix(normalized[idx+len(marker):], "/"), true
|
||||
}
|
||||
prefix := from + "/"
|
||||
if strings.HasPrefix(normalized, prefix) {
|
||||
return strings.TrimPrefix(normalized[len(prefix):], "/"), true
|
||||
}
|
||||
if strings.Trim(normalized, "/") == from {
|
||||
return "", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func localUploadRelativePath(filePath string) string {
|
||||
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(filePath)))
|
||||
for _, marker := range []string{
|
||||
"resource/upload/",
|
||||
"/resource/upload/",
|
||||
"/app/resource/upload/",
|
||||
"/app/upload/",
|
||||
"upload/",
|
||||
"/upload/",
|
||||
} {
|
||||
if idx := strings.Index(normalized, marker); idx >= 0 {
|
||||
return strings.TrimPrefix(normalized[idx+len(marker):], "/")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func joinUploadURL(base string, suffix string) string {
|
||||
base = strings.TrimRight(strings.TrimSpace(filepath.ToSlash(base)), "/")
|
||||
suffix = strings.TrimLeft(strings.TrimSpace(filepath.ToSlash(suffix)), "/")
|
||||
if base == "" {
|
||||
base = "/upload"
|
||||
}
|
||||
if suffix == "" {
|
||||
return base
|
||||
}
|
||||
return base + "/" + suffix
|
||||
}
|
||||
|
||||
func TaskName(batchType int) string {
|
||||
if batchType == 11 {
|
||||
return "单卡加油包结果"
|
||||
}
|
||||
if batchType == 12 {
|
||||
return "流量池加油包结果"
|
||||
}
|
||||
switch batchType {
|
||||
case 1:
|
||||
return "SIM卡批量入库结果"
|
||||
case 3:
|
||||
return "卡划拨结果"
|
||||
case 4:
|
||||
return "卡续费/变更预处理结果"
|
||||
case 101:
|
||||
return "卡续费结果"
|
||||
case 102:
|
||||
return "卡变更套餐结果"
|
||||
case 103:
|
||||
return "SIM卡批量复机结果"
|
||||
case 104:
|
||||
return "SIM卡批量停机结果"
|
||||
case 105:
|
||||
return "SIM卡批量重启结果"
|
||||
case 106:
|
||||
return "卡销户结果"
|
||||
case 108:
|
||||
return "SIM卡批量退订结果"
|
||||
case 107:
|
||||
return "SIM卡达量断网结果"
|
||||
case 109:
|
||||
return "卡开数据结果"
|
||||
case 110:
|
||||
return "卡停数据结果"
|
||||
case 40:
|
||||
return "SIM卡预警配置结果"
|
||||
case 41:
|
||||
return "达量断网配置结果"
|
||||
default:
|
||||
return fmt.Sprintf("SIM卡批量任务%d结果", batchType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package batchresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestWriteDataServiceWorkbookPreservesReviewStatusAndReason(t *testing.T) {
|
||||
for _, batchType := range []int{consts.BatchTypeDataCut, consts.BatchTypeDataStart, consts.BatchTypeDataStop} {
|
||||
t.Run(TaskName(batchType), func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "result.xlsx")
|
||||
rows := []Row{
|
||||
{RowNo: 1, ICCID: "test-success", DetailStatus: consts.BatchDetailStatusSuccess, Success: true},
|
||||
{RowNo: 2, ICCID: "test-failed", DetailStatus: consts.BatchDetailStatusFailed, Reason: "上游明确拒绝"},
|
||||
{RowNo: 3, ICCID: "test-unknown", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "查询超时,尚未确认上游执行结果", UpstreamCode: 202, OrderNum: "test-job-id"},
|
||||
{RowNo: 4, ICCID: "test-review-priority", DetailStatus: consts.BatchDetailStatusUnknown, Success: true, Reason: "明细仍待核查"},
|
||||
{RowNo: 5, ICCID: "test-legacy-success", Success: true},
|
||||
{RowNo: 6, ICCID: "test-legacy-failed", Reason: "未受理"},
|
||||
}
|
||||
if err := writeWorkbook(path, Request{BatchType: batchType, Rows: rows}); err != nil {
|
||||
t.Fatalf("写入数据服务结果失败:%v", err)
|
||||
}
|
||||
file, err := excelize.OpenFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("打开数据服务结果失败:%v", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
for cell, want := range map[string]string{
|
||||
"G1": "执行结果", "H1": "失败/待核查原因",
|
||||
"G2": "成功", "G3": "失败", "H3": "上游明确拒绝",
|
||||
"G4": "待核查", "H4": rows[2].Reason, "I4": "202", "J4": "test-job-id",
|
||||
"G5": "待核查", "H5": rows[3].Reason,
|
||||
"G6": "成功", "G7": "失败", "H7": "未受理",
|
||||
} {
|
||||
got, err := file.GetCellValue("处理结果", cell)
|
||||
if err != nil || got != want {
|
||||
t.Fatalf("单元格 %s = %q,预期 %q,错误:%v", cell, got, want, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSameBatchUsesIndependentResultFiles(t *testing.T) {
|
||||
// Keep the generated workbook path below Excel's 207-character limit on Windows.
|
||||
root, rootErr := os.MkdirTemp("", "batch-result-")
|
||||
if rootErr != nil {
|
||||
t.Fatal(rootErr)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(root); err != nil {
|
||||
t.Errorf("remove test result directory: %v", err)
|
||||
}
|
||||
})
|
||||
t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root)
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
adapter, err := gcfg.NewAdapterContent(`
|
||||
system:
|
||||
upload:
|
||||
local:
|
||||
path: "resource/upload"
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) })
|
||||
request := Request{BatchNo: "DATA-REVIEW", BatchType: consts.BatchTypeDataStart, TenantID: "tenant", Rows: []Row{{RowNo: 1, ICCID: "first-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}}}
|
||||
requests := []Request{request, request}
|
||||
requests[1].Rows = []Row{{RowNo: 1, ICCID: "second-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}}
|
||||
artifacts := make([]Artifact, 2)
|
||||
errors := make([]error, 2)
|
||||
var workers sync.WaitGroup
|
||||
for index := range requests {
|
||||
workers.Add(1)
|
||||
go func(index int) {
|
||||
defer workers.Done()
|
||||
artifacts[index], errors[index] = New().Create(context.Background(), requests[index])
|
||||
}(index)
|
||||
}
|
||||
workers.Wait()
|
||||
for _, err := range errors {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
first, second := artifacts[0], artifacts[1]
|
||||
defer RemoveLocalFile(first)
|
||||
defer RemoveLocalFile(second)
|
||||
if first.Path == second.Path || first.LocalPath == second.LocalPath || first.FileName == second.FileName {
|
||||
t.Fatalf("同批结果物理文件冲突: first=%+v second=%+v", first, second)
|
||||
}
|
||||
if first.OriginalName != "DATA-REVIEW-result.xlsx" || second.OriginalName != first.OriginalName {
|
||||
t.Fatalf("用户下载原始文件名被更改: %q / %q", first.OriginalName, second.OriginalName)
|
||||
}
|
||||
RemoveLocalFile(first)
|
||||
if _, err := os.Stat(first.LocalPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("首次生成文件未删除: %v", err)
|
||||
}
|
||||
file, err := excelize.OpenFile(second.LocalPath)
|
||||
if err != nil {
|
||||
t.Fatalf("删除第一份后第二份不可读: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
got, err := file.GetCellValue("处理结果", "B2")
|
||||
if err != nil || got != "second-card" {
|
||||
t.Fatalf("第二份结果内容损坏: B2=%q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePreservesWorkbookAndPermissionsForAllBatchTypes(t *testing.T) {
|
||||
root, err := os.MkdirTemp("", "batchresult-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(root) })
|
||||
t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root)
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
adapter, err := gcfg.NewAdapterContent(`
|
||||
system:
|
||||
upload:
|
||||
local:
|
||||
path: "resource/upload"
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) })
|
||||
for _, batchType := range []int{1, 3, 4, 11, 12, 40, 41, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110} {
|
||||
t.Run(strconv.Itoa(batchType), func(t *testing.T) {
|
||||
request := Request{BatchNo: "COMPAT-" + strconv.Itoa(batchType), BatchType: batchType, TenantID: "tenant", Rows: []Row{
|
||||
{RowNo: 1, ICCID: "test-success", ObjectCode: "test-success", Success: true},
|
||||
{RowNo: 2, ICCID: "test-failed", ObjectCode: "test-failed", Reason: "原有失败原因"},
|
||||
}}
|
||||
baseline := filepath.Join(t.TempDir(), "baseline.xlsx")
|
||||
if err := writeWorkbook(baseline, request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artifact, err := New().Create(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer RemoveLocalFile(artifact)
|
||||
if artifact.OriginalName != request.BatchNo+"-result.xlsx" || filepath.Ext(artifact.LocalPath) != ".xlsx" {
|
||||
t.Fatalf("结果文件名或后缀发生变化: %+v", artifact)
|
||||
}
|
||||
baselineInfo, err := os.Stat(baseline)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actualInfo, err := os.Stat(artifact.LocalPath)
|
||||
if err != nil || actualInfo.Mode().Perm() != baselineInfo.Mode().Perm() {
|
||||
t.Fatalf("结果文件权限未保留原SaveAs行为: %v", err)
|
||||
}
|
||||
before, err := excelize.OpenFile(baseline)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer before.Close()
|
||||
after, err := excelize.OpenFile(artifact.LocalPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer after.Close()
|
||||
want, err := before.GetRows("处理结果")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := after.GetRows("处理结果")
|
||||
if err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("随机物理名改变了批次%d工作簿内容: err=%v", batchType, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWritesToMountedUploadAndKeepsLogicalPath(t *testing.T) {
|
||||
wd := t.TempDir()
|
||||
oldWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("get working directory: %v", err)
|
||||
}
|
||||
if err = os.Chdir(wd); err != nil {
|
||||
t.Fatalf("change working directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
if err = os.MkdirAll(filepath.Join(wd, "upload"), 0o755); err != nil {
|
||||
t.Fatalf("create mounted upload root: %v", err)
|
||||
}
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
adapter, err := gcfg.NewAdapterContent(`
|
||||
system:
|
||||
upload:
|
||||
local:
|
||||
path: "resource/upload"
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("create config adapter: %v", err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
defer g.Cfg().SetAdapter(oldAdapter)
|
||||
|
||||
artifact, err := New().Create(context.Background(), Request{
|
||||
BatchNo: "INB202608140001001",
|
||||
BatchType: 1,
|
||||
TenantID: "000000",
|
||||
Rows: []Row{{RowNo: 1, ICCID: "89860401786527567308", Success: true}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
defer RemoveLocalFile(artifact)
|
||||
|
||||
logicalPath := filepath.ToSlash(artifact.Path)
|
||||
if !strings.HasPrefix(logicalPath, "resource/upload/000000/user/file/") {
|
||||
t.Fatalf("logical path = %q, want resource/upload path", artifact.Path)
|
||||
}
|
||||
localPath := filepath.ToSlash(artifact.LocalPath)
|
||||
if !strings.HasPrefix(localPath, "upload/000000/user/file/") {
|
||||
t.Fatalf("local path = %q, want mounted upload path", artifact.LocalPath)
|
||||
}
|
||||
if _, err = os.Stat(artifact.LocalPath); err != nil {
|
||||
t.Fatalf("result file was not written at local path %q: %v", artifact.LocalPath, err)
|
||||
}
|
||||
if _, err = os.Stat(artifact.Path); !os.IsNotExist(err) {
|
||||
t.Fatalf("result file should not be written to logical-only path %q, stat error=%v", artifact.Path, err)
|
||||
}
|
||||
if !strings.HasPrefix(artifact.URL, "/upload/000000/user/file/") {
|
||||
t.Fatalf("url = %q, want /upload URL", artifact.URL)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user