1042 lines
34 KiB
Go
1042 lines
34 KiB
Go
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
|
||
}
|