Files
iotlcm/admin-svr/server/internal/logic/iot/t_sim_card_allocate.go
T

1042 lines
34 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package iot
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/allocate"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/packagecustomer"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize"
"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/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/xuri/excelize/v2"
)
const (
allocatePrepareBatchType = consts.BatchTypePrepare
allocateEventQueue = "iotlcm.app.business"
allocateEventType = "card_allocate_completed"
)
type sIotTSimCardAllocate struct{}
type allocateRow struct {
DetailID int64
RowNo int
Iccid string
Msisdn string
Imsi string
TargetDeptId int64
TargetDeptKey string
TargetDeptName string
Target *entity.SysDept
Remark string
Card *entity.TSimCardInfo
Reason string
}
func NewIotTSimCardAllocate() *sIotTSimCardAllocate {
return &sIotTSimCardAllocate{}
}
func init() {
service.RegisterIotTSimCardAllocate(NewIotTSimCardAllocate())
}
func (s *sIotTSimCardAllocate) DownloadExcelTemplate(ctx context.Context) error {
book := excelize.NewFile()
defer func() { _ = book.Close() }()
headers := allocate.NewProcessor().Config().TemplateHeaders
if err := book.SetSheetRow("Sheet1", "A1", &headers); err != nil {
return err
}
widths := map[string]float64{
"A": 22,
"B": 20,
"C": 20,
"D": 28,
}
for column, width := range widths {
if err := book.SetColWidth("Sheet1", column, column, width); err != nil {
return err
}
}
if err := iot_excelize.ApplyHeaderColumnsTextStyle(book); err != nil {
return err
}
return iot_excelize.WriteHTTP(ctx, book, "sim_card_allocate_template")
}
func (s *sIotTSimCardAllocate) ListTargets(ctx context.Context, in *model.TSimCardAllocateTargetListParam) (items []*model.TSimCardAllocateTargetModel, err error) {
operatorDeptID := contexts.GetDeptId(ctx)
if operatorDeptID <= 0 {
return nil, gerror.New("当前账号未绑定部门,不能使用卡划拨")
}
columns := dao.SysDept.Columns()
mod := handler.Model(dao.SysDept.Ctx(ctx), &handler.Option{
FilterTenant: true,
FilterAuth: true,
}).Where(columns.ParentId, operatorDeptID).
Where(columns.Status, "0").
WhereNull(columns.DeletedAt).
OrderAsc(columns.OrderNum).
OrderAsc(columns.DeptId)
if in != nil {
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
mod = mod.Where(
"("+columns.DeptName+" LIKE ? OR "+columns.DeptKey+" LIKE ? OR "+columns.DeptId+" = ?)",
"%"+keyword+"%", "%"+keyword+"%", keyword,
)
}
}
var depts []*entity.SysDept
if err = mod.Scan(&depts); err != nil {
return nil, gerror.Wrap(err, "查询划拨目标客户失败")
}
items = make([]*model.TSimCardAllocateTargetModel, 0, len(depts))
for _, dept := range depts {
if dept == nil || dept.DeptId <= 0 {
continue
}
items = append(items, &model.TSimCardAllocateTargetModel{
DeptId: dept.DeptId,
ParentId: dept.ParentId,
DeptKey: strings.TrimSpace(dept.DeptKey),
DeptName: strings.TrimSpace(dept.DeptName),
DeptCategory: strings.TrimSpace(dept.DeptCategory),
})
}
return items, nil
}
func (s *sIotTSimCardAllocate) PrepareInput(ctx context.Context, in *model.TSimCardAllocatePrepareParam) (*model.TSimCardAllocateBatchModel, error) {
if in == nil {
in = &model.TSimCardAllocatePrepareParam{}
}
result, err := allocate.CreatePendingInput(ctx, in)
if err != nil {
return nil, err
}
return allocatePendingResultToModel(result), nil
}
func (s *sIotTSimCardAllocate) PrepareExcel(ctx context.Context, in *model.TSimCardAllocateExcelSubmitParam) (*model.TSimCardAllocateBatchModel, error) {
if in == nil {
in = &model.TSimCardAllocateExcelSubmitParam{}
}
file, err := allocateUploadFile(ctx)
if err != nil {
return nil, err
}
result, err := allocate.CreatePendingExcel(ctx, file, in)
if err != nil {
return nil, err
}
return allocatePendingResultToModel(result), nil
}
func allocatePendingResultToModel(result *allocate.PendingResult) *model.TSimCardAllocateBatchModel {
res := &model.TSimCardAllocateBatchModel{
BatchType: consts.BatchTypeAllocate,
Status: "pending",
Details: []*model.TSimCardAllocateBatchDetailModel{},
BatchAsyncStatus: consts.BatchAsyncStatusPending,
}
if result == nil {
return res
}
res.BatchNo = result.BatchNo
res.BatchType = result.BatchType
res.BatchAsyncStatus = result.BatchAsyncStatus
res.TotalCount = result.Total
res.ValidCount = result.ValidCount
res.FailCount = result.FailCount
return res
}
func (s *sIotTSimCardAllocate) prepare(ctx context.Context, rows []*allocateRow, targetDeptID int64, source string) *model.TSimCardAllocateBatchModel {
result, err := s.createPrepareBatch(ctx, rows, targetDeptID, source)
if err != nil {
return &model.TSimCardAllocateBatchModel{
BatchType: allocatePrepareBatchType,
Status: "failed",
FailReason: err.Error(),
}
}
return result
}
func (s *sIotTSimCardAllocate) createPrepareBatch(ctx context.Context, rows []*allocateRow, targetDeptID int64, source string) (*model.TSimCardAllocateBatchModel, error) {
operator := allocateOperator(ctx)
if operator.DeptId <= 0 {
return nil, gerror.New("当前账号未绑定部门,不能使用卡划拨")
}
if targetDeptID <= 0 && source != "excel" {
return nil, gerror.New("请选择划拨目标客户")
}
var target *entity.SysDept
if targetDeptID > 0 {
var err error
target, err = s.loadTarget(ctx, operator.TenantId, operator.DeptId, targetDeptID)
if err != nil {
return nil, err
}
}
if len(rows) == 0 {
return nil, gerror.New("划拨卡片不能为空")
}
if len(rows) > 1000 {
return nil, gerror.New("单次最多划拨1000张卡")
}
s.validateRows(ctx, operator, rows, target)
batchNo, err := nextAllocateBatchNo(ctx)
if err != nil {
return nil, err
}
now := gtime.Now()
validCount, failCount := allocateCounts(rows)
failReason := ""
if failCount > 0 {
failReason = "存在校验失败数据,本次未执行"
}
units, err := allocationunits.Snapshot(ctx, operator.TenantId, operator.DeptId, allocateTargetDeptID(target))
if err != nil {
return nil, err
}
batchInfo := gjson.New(g.Map{
"source": source,
"targetBatchType": consts.BatchTypeAllocate,
"targetBatchName": "卡划拨",
"targetDeptId": allocateTargetDeptID(target),
"targetDeptName": allocateTargetDeptName(target),
"targetMode": allocateTargetMode(target, rows),
"quantity": validCount,
"totalCount": len(rows),
"validCount": validCount,
"failCount": failCount,
"failReason": failReason,
})
batchInfo = allocationunits.Merge(batchInfo, units)
batchStatus := consts.BatchAsyncStatusSuccess
runResult := consts.BatchRunResultSuccess
if failCount > 0 {
batchStatus = consts.BatchAsyncStatusFailed
runResult = consts.BatchRunResultFailed
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(do.BatchMain{
BatchNo: batchNo,
BatchType: allocatePrepareBatchType,
BatchAsyncStatus: batchStatus,
BatchStatus: consts.BatchStatusNormal,
SourceType: allocateSourceType(source),
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
TargetDeptId: allocateTargetDeptID(target),
DeptId: operator.DeptId,
TotalCount: len(rows),
ValidCount: validCount,
SuccessCount: 0,
FailCount: failCount,
RunResult: runResult,
FailReason: failReason,
BatchInfo: batchInfo,
StartTime: now,
EndTime: now,
TenantId: operator.TenantId,
CreatedDept: operator.DeptId,
CreatedBy: operator.UserId,
CreatedAt: now,
UpdatedBy: operator.UserId,
UpdatedAt: now,
}).Insert()
if err != nil {
return gerror.Wrap(err, "写入划拨预处理批次失败")
}
details := make([]do.BatchDetail, 0, len(rows))
for _, row := range rows {
status := consts.BatchDetailStatusSuccess
if row.Reason != "" {
status = consts.BatchDetailStatusFailed
}
details = append(details, do.BatchDetail{
BatchNo: batchNo,
BatchType: allocatePrepareBatchType,
DetailStatus: status,
BlankOutStatus: consts.BlankOutStatusNo,
RowNo: row.RowNo,
ItemId: row.Iccid,
ErrorReason: row.Reason,
Snapshot: allocationunits.Merge(allocateRowSnapshot(row, target), gjson.New(map[string]any{"sourceDeptName": units.Get("sourceDeptName").String(), "sourceDeptKey": units.Get("sourceDeptKey").String()})),
TenantId: operator.TenantId,
CreatedDept: operator.DeptId,
CreatedBy: operator.UserId,
CreatedAt: now,
UpdatedBy: operator.UserId,
UpdatedAt: now,
})
}
_, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).Data(details).Insert()
return gerror.Wrap(err, "写入划拨预处理明细失败")
})
if err != nil {
return nil, err
}
return buildAllocateBatchModel(batchNo, allocatePrepareBatchType, target, rows, "prepared", failReason), nil
}
func (s *sIotTSimCardAllocate) SubmitBatch(ctx context.Context, in *model.TSimCardAllocateBatchSubmitParam) (*model.TSimCardAllocateBatchModel, error) {
if in == nil {
in = &model.TSimCardAllocateBatchSubmitParam{}
}
operator := allocateOperator(ctx)
batchNo := strings.TrimSpace(in.BatchNo)
if batchNo == "" {
return nil, gerror.New("预处理批次号不能为空")
}
var result *model.TSimCardAllocateBatchModel
var eventTarget *entity.SysDept
var syncDeptIDs []int64
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var err error
var batch *entity.BatchMain
if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Where(dao.BatchMain.Columns().BatchType, allocatePrepareBatchType).
Where(dao.BatchMain.Columns().TenantId, operator.TenantId).
Where(dao.BatchMain.Columns().CreatedBy, operator.UserId).
LockUpdate().Scan(&batch); err != nil {
return gerror.Wrap(err, "查询划拨预处理批次失败")
}
if batch == nil || batch.Id == 0 {
return gerror.New("划拨预处理批次不存在或无权限确认")
}
if batch.BatchInfo == nil ||
batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeAllocate {
return gerror.New("批次不是卡划拨预处理批次")
}
if batch.BatchAsyncStatus != consts.BatchAsyncStatusSuccess ||
batch.FailCount > 0 ||
batch.RunResult != consts.BatchRunResultSuccess {
return gerror.New("划拨预处理批次存在校验失败数据,请重新校验")
}
if batch.TargetDeptId > 0 && in.TargetDeptId > 0 && in.TargetDeptId != batch.TargetDeptId {
return gerror.New("提交目标客户与预处理批次不一致,请重新校验")
}
var details []*entity.BatchDetail
if err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, allocatePrepareBatchType).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess).
OrderAsc(dao.BatchDetail.Columns().RowNo).
Scan(&details); err != nil {
return gerror.Wrap(err, "查询划拨预处理明细失败")
}
if len(details) == 0 {
return gerror.New("划拨预处理批次没有可确认的卡片")
}
rows := make([]*allocateRow, 0, len(details))
targets := make(map[int64]*entity.SysDept)
for _, detail := range details {
if detail == nil || strings.TrimSpace(detail.ItemId) == "" {
return gerror.New("划拨预处理明细缺少ICCID")
}
if detail.Snapshot == nil {
return gerror.Newf("卡%s缺少预校验快照,请重新校验", detail.ItemId)
}
cardID := detail.Snapshot.Get("cardId").Int64()
if cardID <= 0 {
return gerror.Newf("卡%s缺少有效卡片ID,请重新校验", detail.ItemId)
}
targetDeptID := detail.Snapshot.Get("targetDeptId").Int64()
if targetDeptID <= 0 {
targetDeptID = batch.TargetDeptId
}
if targetDeptID <= 0 {
return gerror.Newf("卡%s缺少目标客户快照,请重新校验", detail.ItemId)
}
target := targets[targetDeptID]
if target == nil {
target, err = s.loadTargetTx(ctx, tx, operator.TenantId, operator.DeptId, targetDeptID)
if err != nil {
return err
}
targets[targetDeptID] = target
}
var card *entity.TSimCardInfo
if err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(dao.TSimCardInfo.Columns().Id, cardID).
Where(dao.TSimCardInfo.Columns().TenantId, operator.TenantId).
LockUpdate().Scan(&card); err != nil {
return err
}
if card == nil || card.Id == 0 {
return gerror.Newf("卡%s不存在,不能划拨", detail.ItemId)
}
if card.DeptId != operator.DeptId {
return gerror.Newf("卡%s归属已变更,请重新校验", card.Iccid)
}
if reason := validateAllocateCard(card, target.DeptId); reason != "" {
return gerror.New(reason)
}
rows = append(rows, &allocateRow{
DetailID: detail.Id,
RowNo: detail.RowNo,
Iccid: strings.TrimSpace(card.Iccid),
Msisdn: strings.TrimSpace(card.Msisdn),
Imsi: strings.TrimSpace(card.Imsi),
TargetDeptId: target.DeptId,
TargetDeptKey: strings.TrimSpace(target.DeptKey),
TargetDeptName: strings.TrimSpace(target.DeptName),
Target: target,
Card: card,
})
}
eventTarget = singleAllocateTarget(rows)
syncDeptIDs = allocatePackageSyncDeptIDs(operator, rows, eventTarget)
now := gtime.Now()
for _, row := range rows {
target := row.Target
if target == nil || target.DeptId <= 0 {
return gerror.Newf("卡%s缺少目标客户快照,请重新校验", row.Iccid)
}
affected, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(dao.TSimCardInfo.Columns().Id, row.Card.Id).
Where(dao.TSimCardInfo.Columns().DeptId, operator.DeptId).
Data(do.TSimCardInfo{
DeptId: target.DeptId,
OutboundBatchNo: batchNo,
AllocationBatchNo: batchNo,
UpdatedBy: operator.UserId,
UpdatedAt: now,
}).Update()
if err != nil {
return gerror.Wrapf(err, "更新卡%s归属失败", row.Iccid)
}
if count, err := affected.RowsAffected(); err != nil || count != 1 {
return gerror.Newf("卡%s归属已变更,请重新校验", row.Iccid)
}
if _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, row.DetailID).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Data(do.BatchDetail{
BatchType: consts.BatchTypeAllocate,
Snapshot: allocateSubmitSnapshot(row, target, batchNo),
UpdatedBy: operator.UserId,
UpdatedAt: now,
}).Update(); err != nil {
return gerror.Wrap(err, "更新划拨批次明细失败")
}
}
batchInfo := gjson.New(map[string]any{
"source": "console",
"targetBatchType": consts.BatchTypeAllocate,
"targetBatchName": "卡划拨",
"targetDeptId": allocateTargetDeptID(eventTarget),
"targetDeptName": allocateTargetDeptName(eventTarget),
"targetMode": allocateTargetMode(eventTarget, rows),
"quantity": len(rows),
"remark": strings.TrimSpace(in.Remark),
"allocateTime": now,
})
if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().Id, batch.Id).
Where(dao.BatchMain.Columns().BatchType, allocatePrepareBatchType).
Data(do.BatchMain{
BatchType: consts.BatchTypeAllocate,
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
TargetDeptId: allocateTargetDeptID(eventTarget),
ValidCount: len(rows),
SuccessCount: len(rows),
FailCount: 0,
RunResult: consts.BatchRunResultSuccess,
FailReason: "",
BatchInfo: batchInfo,
EndTime: now,
UpdatedBy: operator.UserId,
UpdatedAt: now,
}).Update(); err != nil {
return gerror.Wrap(err, "更新划拨批次失败")
}
result = buildAllocateBatchModel(batchNo, consts.BatchTypeAllocate, eventTarget, rows, "success", "")
return nil
})
if err != nil {
return nil, err
}
publishAllocateEvent(ctx, operator, batchNo, eventTarget)
packagecustomer.PublishSyncForDeptsSafe(ctx, operator.TenantId, operator.UserId, syncDeptIDs, "allocate", batchNo)
return result, nil
}
func (s *sIotTSimCardAllocate) loadTarget(ctx context.Context, tenantID string, sourceDeptID, targetDeptID int64) (*entity.SysDept, error) {
return s.loadTargetTx(ctx, nil, tenantID, sourceDeptID, targetDeptID)
}
func (s *sIotTSimCardAllocate) loadTargetTx(ctx context.Context, tx gdb.TX, tenantID string, sourceDeptID, targetDeptID int64) (*entity.SysDept, error) {
mod := dao.SysDept.Ctx(ctx)
if tx != nil {
mod = tx.Ctx(ctx).Model(dao.SysDept.Table())
}
columns := dao.SysDept.Columns()
var target *entity.SysDept
err := mod.Where(columns.TenantId, tenantID).
Where(columns.DeptId, targetDeptID).
Where(columns.ParentId, sourceDeptID).
Where(columns.Status, "0").
WhereNull(columns.DeletedAt).
Limit(1).Scan(&target)
if err != nil {
return nil, gerror.Wrap(err, "查询划拨目标客户失败")
}
if target == nil || target.DeptId == 0 {
return nil, gerror.New("划拨目标客户不存在或不在当前部门的下级范围内")
}
return target, nil
}
func (s *sIotTSimCardAllocate) validateRows(ctx context.Context, operator allocateOperatorContext, rows []*allocateRow, fixedTarget *entity.SysDept) {
seen := make(map[string]struct{}, len(rows))
columns := dao.TSimCardInfo.Columns()
for _, row := range rows {
if fixedTarget != nil {
row.TargetDeptId = fixedTarget.DeptId
row.TargetDeptKey = strings.TrimSpace(fixedTarget.DeptKey)
row.TargetDeptName = strings.TrimSpace(fixedTarget.DeptName)
row.Target = fixedTarget
}
row.Iccid = strings.TrimSpace(row.Iccid)
if row.Iccid == "" {
row.Reason = appendAllocateReason(row.Reason, "ICCID不能为空")
continue
}
if _, exists := seen[row.Iccid]; exists {
row.Reason = appendAllocateReason(row.Reason, "ICCID重复")
continue
}
seen[row.Iccid] = struct{}{}
var card *entity.TSimCardInfo
err := handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{
FilterTenant: true,
FilterAuth: true,
}).Where(columns.Iccid, row.Iccid).Limit(1).Scan(&card)
if err != nil || card == nil || card.Id == 0 {
row.Reason = appendAllocateReason(row.Reason, "ICCID不存在或不在当前操作员授权范围内")
continue
}
row.Card = card
row.Msisdn = strings.TrimSpace(card.Msisdn)
row.Imsi = strings.TrimSpace(card.Imsi)
if card.DeptId != operator.DeptId {
row.Reason = appendAllocateReason(row.Reason, "卡片当前归属不在当前部门,不能划拨")
continue
}
if row.TargetDeptId <= 0 {
row.Reason = appendAllocateReason(row.Reason, "目标客户不能为空")
continue
}
if reason := validateAllocateCard(card, row.TargetDeptId); reason != "" {
row.Reason = appendAllocateReason(row.Reason, reason)
}
}
}
func validateAllocateCard(card *entity.TSimCardInfo, targetDeptID int64) string {
if card == nil {
return "卡片信息不存在"
}
label := strings.TrimSpace(card.Iccid)
if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
return fmt.Sprintf("卡%s当前为停机状态,不能划拨", label)
}
if card.PackageStatus == consts.PackageStatusExpired ||
(card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() && card.FlowPkgEndAt.Before(gtime.Now())) {
return fmt.Sprintf("卡%s套餐已过期,不能划拨", label)
}
if card.DeptId == targetDeptID {
return fmt.Sprintf("卡%s已属于目标客户,无需重复划拨", label)
}
return ""
}
func allocateCounts(rows []*allocateRow) (valid, failed int) {
for _, row := range rows {
if row != nil && row.Reason == "" {
valid++
} else {
failed++
}
}
return
}
func buildAllocateBatchModel(batchNo string, batchType int, target *entity.SysDept, rows []*allocateRow, status, failReason string) *model.TSimCardAllocateBatchModel {
valid, failed := allocateCounts(rows)
result := &model.TSimCardAllocateBatchModel{
BatchNo: batchNo,
BatchType: batchType,
TotalCount: len(rows),
ValidCount: valid,
FailCount: failed,
FailReason: failReason,
Status: status,
Details: make([]*model.TSimCardAllocateBatchDetailModel, 0, len(rows)),
}
if target != nil {
result.Target = &model.TSimCardAllocateTargetModel{
DeptId: target.DeptId,
ParentId: target.ParentId,
DeptKey: strings.TrimSpace(target.DeptKey),
DeptName: strings.TrimSpace(target.DeptName),
DeptCategory: strings.TrimSpace(target.DeptCategory),
}
}
for _, row := range rows {
if row == nil {
continue
}
itemStatus := "success"
if row.Reason != "" {
itemStatus = "failed"
}
result.Details = append(result.Details, &model.TSimCardAllocateBatchDetailModel{
RowNo: row.RowNo,
ItemId: row.Iccid,
TargetDeptId: row.TargetDeptId,
TargetDeptKey: row.TargetDeptKey,
TargetDeptName: row.TargetDeptName,
Status: itemStatus,
ErrorReason: row.Reason,
})
}
return result
}
func allocateRowSnapshot(row *allocateRow, target *entity.SysDept) *gjson.Json {
targetDeptID := int64(0)
targetDeptKey := ""
targetDeptName := ""
if row != nil {
targetDeptID = row.TargetDeptId
targetDeptKey = strings.TrimSpace(row.TargetDeptKey)
targetDeptName = strings.TrimSpace(row.TargetDeptName)
}
if targetDeptID <= 0 && target != nil {
targetDeptID = target.DeptId
targetDeptKey = strings.TrimSpace(target.DeptKey)
targetDeptName = strings.TrimSpace(target.DeptName)
}
return gjson.New(g.Map{
"ICCID": row.Iccid,
"MSISDN": row.Msisdn,
"IMSI": row.Imsi,
"cardId": allocateCardID(row),
"sourceDeptId": allocateCardDeptID(row),
"targetDeptId": targetDeptID,
"targetDeptKey": targetDeptKey,
"targetDeptName": targetDeptName,
"remark": row.Remark,
"errorReason": row.Reason,
})
}
func allocateSubmitSnapshot(row *allocateRow, target *entity.SysDept, batchNo string) *gjson.Json {
snapshot := allocateRowSnapshot(row, target).Map()
snapshot["allocateBatchNo"] = batchNo
return gjson.New(snapshot)
}
func allocateCardID(row *allocateRow) int64 {
if row != nil && row.Card != nil {
return row.Card.Id
}
return 0
}
func allocateCardDeptID(row *allocateRow) int64 {
if row != nil && row.Card != nil {
return row.Card.DeptId
}
return 0
}
func allocateTargetDeptID(target *entity.SysDept) int64 {
if target == nil {
return 0
}
return target.DeptId
}
func allocateTargetDeptName(target *entity.SysDept) string {
if target == nil {
return ""
}
return strings.TrimSpace(target.DeptName)
}
func allocateTargetMode(target *entity.SysDept, rows []*allocateRow) string {
if target != nil {
return "single"
}
seen := make(map[int64]struct{})
for _, row := range rows {
if row == nil || row.TargetDeptId <= 0 {
continue
}
seen[row.TargetDeptId] = struct{}{}
}
if len(seen) > 1 {
return "multiple"
}
return "row"
}
func singleAllocateTarget(rows []*allocateRow) *entity.SysDept {
var target *entity.SysDept
for _, row := range rows {
if row == nil || row.Target == nil || row.Target.DeptId <= 0 {
continue
}
if target == nil {
target = row.Target
continue
}
if target.DeptId != row.Target.DeptId {
return nil
}
}
return target
}
type allocateOperatorContext struct {
TenantId string
DeptId int64
UserId int64
}
func allocateOperator(ctx context.Context) allocateOperatorContext {
return allocateOperatorContext{
TenantId: contexts.GetTenantId(ctx),
DeptId: contexts.GetDeptId(ctx),
UserId: contexts.GetUserId(ctx),
}
}
func nextAllocateBatchNo(ctx context.Context) (string, error) {
return batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixAllocate)
}
func publishAllocateEvent(ctx context.Context, operator allocateOperatorContext, batchNo string, target *entity.SysDept) {
payload := g.Map{
"eventType": allocateEventType,
"batchNo": batchNo,
"batchType": consts.BatchTypeAllocate,
"tenantId": operator.TenantId,
"userId": operator.UserId,
"deptId": operator.DeptId,
"trigger": "allocate_submit",
"createdAt": gtime.Now().Format("Y-m-d H:i:s"),
}
if target != nil {
payload["targetDeptId"] = target.DeptId
payload["targetDeptName"] = target.DeptName
}
if err := rabbitmq.Publish(ctx, allocateEventQueue, payload); err != nil {
g.Log().Warningf(ctx, "卡划拨业务事件发布失败,batchNo:%s, queue:%s, err:%+v", batchNo, allocateEventQueue, err)
return
}
g.Log().Infof(ctx, "卡划拨业务事件发布成功,batchNo:%s, queue:%s", batchNo, allocateEventQueue)
}
func allocatePackageSyncDeptIDs(operator allocateOperatorContext, rows []*allocateRow, target *entity.SysDept) []int64 {
deptIds := []int64{operator.DeptId}
if target != nil && target.DeptId > 0 {
deptIds = append(deptIds, target.DeptId)
}
for _, row := range rows {
if row == nil {
continue
}
if row.TargetDeptId > 0 {
deptIds = append(deptIds, row.TargetDeptId)
}
}
return deptIds
}
func parseAllocateInput(input string) ([]*allocateRow, error) {
input = strings.TrimSpace(input)
if input == "" {
return nil, gerror.New("批量输入内容不能为空")
}
lines := strings.FieldsFunc(input, func(r rune) bool {
return r == '\n' || r == '\r'
})
rows := make([]*allocateRow, 0, len(lines))
for index, line := range lines {
fields := strings.FieldsFunc(line, func(r rune) bool {
return r == '\t' || r == ',' || r == ',' || r == ';' || r == ';' || r == ' '
})
row := &allocateRow{RowNo: index + 1}
switch len(fields) {
case 1:
row.Iccid = strings.TrimSpace(fields[0])
case 3:
row.Iccid = strings.TrimSpace(fields[0])
row.Msisdn = strings.TrimSpace(fields[1])
row.Imsi = strings.TrimSpace(fields[2])
default:
row.Reason = "格式错误,应为 ICCID 或 ICCID MSISDN IMSI"
}
rows = append(rows, row)
}
return rows, nil
}
func (s *sIotTSimCardAllocate) resolveExcelTargets(ctx context.Context, rows []*allocateRow, fallbackTargetDeptID int64) (int64, error) {
operator := allocateOperator(ctx)
if operator.DeptId <= 0 {
return 0, gerror.New("当前账号未绑定部门,不能使用卡划拨")
}
var fallbackTarget *entity.SysDept
var err error
if fallbackTargetDeptID > 0 {
fallbackTarget, err = s.loadTarget(ctx, operator.TenantId, operator.DeptId, fallbackTargetDeptID)
if err != nil {
return 0, err
}
}
targets := make(map[int64]*entity.SysDept)
for _, row := range rows {
if row == nil {
continue
}
if row.TargetDeptId <= 0 && row.TargetDeptKey == "" && row.TargetDeptName == "" && fallbackTarget != nil {
row.TargetDeptId = fallbackTarget.DeptId
row.TargetDeptKey = strings.TrimSpace(fallbackTarget.DeptKey)
row.TargetDeptName = strings.TrimSpace(fallbackTarget.DeptName)
row.Target = fallbackTarget
targets[fallbackTarget.DeptId] = fallbackTarget
continue
}
target, err := s.resolveExcelTarget(ctx, operator, row)
if err != nil {
row.Reason = appendAllocateReason(row.Reason, err.Error())
continue
}
row.TargetDeptId = target.DeptId
row.TargetDeptKey = strings.TrimSpace(target.DeptKey)
row.TargetDeptName = strings.TrimSpace(target.DeptName)
row.Target = target
targets[target.DeptId] = target
}
if len(targets) == 1 {
for targetDeptID := range targets {
return targetDeptID, nil
}
}
return 0, nil
}
func (s *sIotTSimCardAllocate) resolveExcelTarget(ctx context.Context, operator allocateOperatorContext, row *allocateRow) (*entity.SysDept, error) {
if row == nil {
return nil, gerror.New("划拨Excel行为空")
}
if row.TargetDeptId > 0 {
return s.loadTarget(ctx, operator.TenantId, operator.DeptId, row.TargetDeptId)
}
if row.TargetDeptKey != "" {
return s.loadTargetByColumn(ctx, nil, operator.TenantId, operator.DeptId, dao.SysDept.Columns().DeptKey, row.TargetDeptKey)
}
if row.TargetDeptName != "" {
return s.loadTargetByColumn(ctx, nil, operator.TenantId, operator.DeptId, dao.SysDept.Columns().DeptName, row.TargetDeptName)
}
return nil, gerror.New("目标客户不能为空")
}
func (s *sIotTSimCardAllocate) loadTargetByColumn(ctx context.Context, tx gdb.TX, tenantID string, sourceDeptID int64, column string, value string) (*entity.SysDept, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, gerror.New("目标客户不能为空")
}
mod := dao.SysDept.Ctx(ctx)
if tx != nil {
mod = tx.Ctx(ctx).Model(dao.SysDept.Table())
}
columns := dao.SysDept.Columns()
var targets []*entity.SysDept
err := mod.Where(columns.TenantId, tenantID).
Where(columns.ParentId, sourceDeptID).
Where(columns.Status, "0").
WhereNull(columns.DeletedAt).
Where(column, value).
OrderAsc(columns.DeptId).
Limit(2).
Scan(&targets)
if err != nil {
return nil, gerror.Wrap(err, "查询划拨目标客户失败")
}
if len(targets) == 0 {
return nil, gerror.New("划拨目标客户不存在或不在当前部门的下级范围内")
}
if len(targets) > 1 {
return nil, gerror.New("目标客户名称不唯一,请改用批量输入方式选择目标客户")
}
return targets[0], nil
}
func parseAllocateExcel(file *ghttp.UploadFile) ([]*allocateRow, error) {
reader, err := file.Open()
if err != nil {
return nil, gerror.Wrap(err, "打开划拨Excel失败")
}
defer reader.Close()
content, err := io.ReadAll(reader)
if err != nil {
return nil, gerror.Wrap(err, "读取划拨Excel失败")
}
book, err := excelize.OpenReader(bytes.NewReader(content))
if err != nil {
return nil, gerror.Wrap(err, "解析划拨Excel失败")
}
defer book.Close()
sheet := book.GetSheetName(0)
rows, err := book.GetRows(sheet)
if err != nil {
return nil, gerror.Wrap(err, "读取划拨Excel行失败")
}
if len(rows) < 2 {
return nil, gerror.New("划拨Excel没有可处理的数据")
}
headers := make(map[string]int)
for index, header := range rows[0] {
headers[normalizeAllocateHeader(header)] = index
}
iccidIndex, ok := headers[normalizeAllocateHeader("ICCID")]
if !ok {
return nil, gerror.New("划拨Excel必须包含ICCID列")
}
if !hasAllocateHeader(headers, "目标客户名称", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME") {
return nil, gerror.New("划拨Excel必须包含目标客户名称列")
}
result := make([]*allocateRow, 0, len(rows)-1)
for index, values := range rows[1:] {
row := &allocateRow{
RowNo: index + 2,
Iccid: allocateHeaderValue(headers, values, "ICCID"),
Msisdn: allocateHeaderValue(headers, values, "MSISDN"),
Imsi: allocateHeaderValue(headers, values, "IMSI"),
TargetDeptName: allocateHeaderValue(headers, values, "目标客户名称", "客户名称", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME"),
Remark: allocateHeaderValue(headers, values, "划拨备注", "备注", "REMARK"),
}
if iccidIndex >= len(values) {
row.Reason = appendAllocateReason(row.Reason, "ICCID不能为空")
result = append(result, row)
continue
}
if row.TargetDeptId <= 0 && row.TargetDeptKey == "" && row.TargetDeptName == "" {
row.Reason = appendAllocateReason(row.Reason, "目标客户不能为空")
}
result = append(result, row)
}
return result, nil
}
func normalizeAllocateHeader(value string) string {
value = strings.ToUpper(strings.TrimSpace(value))
replacer := strings.NewReplacer(" ", "", "_", "", "-", "", " ", "")
return replacer.Replace(value)
}
func hasAllocateHeader(headers map[string]int, aliases ...string) bool {
for _, alias := range aliases {
if _, ok := headers[normalizeAllocateHeader(alias)]; ok {
return true
}
}
return false
}
func allocateHeaderValue(headers map[string]int, values []string, aliases ...string) string {
for _, alias := range aliases {
column, exists := headers[normalizeAllocateHeader(alias)]
if !exists || column >= len(values) {
continue
}
if value := strings.TrimSpace(values[column]); value != "" {
return value
}
}
return ""
}
func parseAllocateTargetDeptID(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, nil
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed <= 0 {
return 0, gerror.New("目标客户ID格式错误")
}
return parsed, nil
}
func appendAllocateReason(current string, reason string) string {
current = strings.TrimSpace(current)
reason = strings.TrimSpace(reason)
if current == "" {
return reason
}
if reason == "" {
return current
}
return current + ";" + reason
}
func allocateUploadFile(ctx context.Context) (*ghttp.UploadFile, error) {
request := g.RequestFromCtx(ctx)
if request == nil {
return nil, gerror.New("当前上下文不是HTTP请求")
}
file := request.GetUploadFile("file")
if file == nil {
return nil, gerror.New("请选择上传文件")
}
return file, nil
}
func allocateSourceType(source string) int {
if strings.TrimSpace(source) == "excel" {
return consts.BatchSourceTypeFile
}
return consts.BatchSourceTypeBatchInput
}