chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:03 +08:00
422 changed files with 80993 additions and 0 deletions
@@ -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)
}
}