434 lines
13 KiB
Go
434 lines
13 KiB
Go
package outbound
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||
|
||
"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"
|
||
)
|
||
|
||
type Processor struct {
|
||
repo *Repository
|
||
opts PreviewOptions
|
||
config batchimport.ProcessorConfig
|
||
excelSubmitParam *model.TSimCardOutboundSubmitParam
|
||
}
|
||
|
||
func (p *Processor) ExecutePending(ctx context.Context, tx gdb.TX, batchNo string, in *model.TSimCardOutboundSubmitParam, rows []*Row, operator batchimport.OperatorContext, requiredSourceBatchNo string) (*CompleteOutput, error) {
|
||
if strings.TrimSpace(batchNo) == "" {
|
||
return nil, gerror.New("出库批次号不能为空")
|
||
}
|
||
if in == nil {
|
||
return nil, gerror.New("出库参数不能为空")
|
||
}
|
||
if err := p.validateExecuteContext(ctx, tx, in, operator, rows, requiredSourceBatchNo); err != nil {
|
||
return nil, err
|
||
}
|
||
return p.Complete(ctx, tx, &CompleteInput{
|
||
SubmitParam: in,
|
||
BatchNo: batchNo,
|
||
Rows: rows,
|
||
Operator: operator,
|
||
StrictStockIdentity: p.opts.StrictStockIdentity,
|
||
RequiredSourceBatchNo: requiredSourceBatchNo,
|
||
})
|
||
}
|
||
|
||
const (
|
||
outboundCardPackageType = 101
|
||
outboundPoolPackageType = 102
|
||
)
|
||
|
||
func NewProcessor(opts PreviewOptions) *Processor {
|
||
return &Processor{
|
||
repo: NewRepository(),
|
||
opts: opts,
|
||
config: batchimport.ProcessorConfig{
|
||
Code: "sim_card_outbound",
|
||
BatchType: consts.BatchTypeOutbound,
|
||
BatchNoPrefix: consts.BatchNoPrefixOutbound,
|
||
TaskName: consts.ResourceBatchDownloadTaskSimCardOutbound,
|
||
ExportScene: consts.ResourceExportSceneOutbound,
|
||
ImportDir: consts.SimCardOutboundSubDirImportExcel,
|
||
ResultDir: consts.SimCardOutboundSubDirResultExcel,
|
||
MaxFileSize: consts.SimCardOutboundMaxFileSize,
|
||
TemplateHeaders: excelHeaders(),
|
||
RequiredHeaders: excelRequiredHeaders(),
|
||
},
|
||
}
|
||
}
|
||
|
||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||
return p.config
|
||
}
|
||
|
||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||
parsed, err := ParseExcelFile(ctx, filePath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
p.excelSubmitParam = nil
|
||
if parsed != nil {
|
||
p.excelSubmitParam = parsed.SubmitParam
|
||
}
|
||
result := make([]batchimport.Row, 0, len(parsed.Rows))
|
||
for _, row := range parsed.Rows {
|
||
result = append(result, row)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||
outboundRows := make([]*Row, 0, len(rows))
|
||
for _, item := range rows {
|
||
row, ok := item.(*Row)
|
||
if !ok {
|
||
item.AddErrorReason("导入行类型错误")
|
||
continue
|
||
}
|
||
outboundRows = append(outboundRows, row)
|
||
}
|
||
|
||
opts := p.opts
|
||
if opts.DeptId == 0 {
|
||
opts.DeptId = contexts.GetDeptId(ctx)
|
||
}
|
||
return validateRows(ctx, outboundRows, opts, p.repo)
|
||
}
|
||
|
||
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||
if req == nil {
|
||
return gerror.New("出库请求不能为空")
|
||
}
|
||
in := p.submitParams(req.BizParams)
|
||
if in == nil {
|
||
return gerror.New("出库参数类型错误")
|
||
}
|
||
|
||
outboundRows := make([]*Row, 0, len(rows))
|
||
for _, item := range rows {
|
||
row, ok := item.(*Row)
|
||
if !ok {
|
||
return gerror.New("导入行类型错误")
|
||
}
|
||
outboundRows = append(outboundRows, row)
|
||
}
|
||
if err := p.validateExecuteContext(ctx, tx, in, req.Operator, outboundRows, requiredSourceBatchNo(req.BizParams)); err != nil {
|
||
return err
|
||
}
|
||
|
||
output, err := p.Complete(ctx, tx, &CompleteInput{
|
||
SubmitParam: in,
|
||
BatchNo: req.BatchNo,
|
||
Rows: outboundRows,
|
||
Operator: req.Operator,
|
||
StrictStockIdentity: p.opts.StrictStockIdentity,
|
||
RequiredSourceBatchNo: requiredSourceBatchNo(req.BizParams),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
req.ExecutionResult = output
|
||
return nil
|
||
}
|
||
|
||
func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||
info := g.Map{}
|
||
info["strictStockIdentity"] = p.opts.StrictStockIdentity
|
||
if result != nil && result.ImportFileOssId > 0 {
|
||
info["importFileOssId"] = result.ImportFileOssId
|
||
} else if importOss != nil {
|
||
info["importFileOssId"] = importOss.OssId
|
||
}
|
||
if resultOss != nil {
|
||
info["resultFileOssId"] = resultOss.OssId
|
||
}
|
||
if downloadId > 0 {
|
||
info["resultDownloadId"] = downloadId
|
||
}
|
||
if in := p.submitParams(req.BizParams); in != nil {
|
||
normalizeSubmitParam(in)
|
||
info["customerAgentId"] = in.CustomerAgentId
|
||
info["packageCode"] = strings.TrimSpace(in.PackageCode)
|
||
info["packageBeginTime"] = in.PackageBeginTime
|
||
info["durationYears"] = in.DurationYears
|
||
info["durationMonthPart"] = in.DurationMonthPart
|
||
info["durationMonths"] = in.DurationMonths
|
||
info["giftDurationYears"] = in.GiftDurationYears
|
||
info["giftDurationMonthPart"] = in.GiftDurationMonthPart
|
||
info["giftDurationMonths"] = in.GiftDurationMonths
|
||
info["amount"] = strings.TrimSpace(in.Amount)
|
||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||
info["remark"] = normalizedRemark(in)
|
||
info["buyerNote"] = normalizedRemark(in)
|
||
info["adminNote"] = normalizedRemark(in)
|
||
}
|
||
if in, ok := req.BizParams.(*model.TSimCardOutboundRangeSubmitParam); ok && in != nil {
|
||
info["rangeCodeType"] = normalizeRangeCodeType(in.RangeCodeType)
|
||
info["startIccid"] = strings.TrimSpace(in.StartIccid)
|
||
info["endIccid"] = strings.TrimSpace(in.EndIccid)
|
||
info["startMsisdn"] = strings.TrimSpace(in.StartMsisdn)
|
||
info["endMsisdn"] = strings.TrimSpace(in.EndMsisdn)
|
||
info["startImsi"] = strings.TrimSpace(in.StartImsi)
|
||
info["endImsi"] = strings.TrimSpace(in.EndImsi)
|
||
}
|
||
if in, ok := req.BizParams.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil {
|
||
info["inboundBatchNo"] = strings.TrimSpace(in.BatchNo)
|
||
}
|
||
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
|
||
info["orderSn"] = output.OrderSn
|
||
info["quantity"] = output.Quantity
|
||
info["sourceBatchNos"] = output.SourceBatchNos
|
||
info["packageBeginTime"] = output.PackageBeginAt
|
||
info["packageEndTime"] = output.PackageEndAt
|
||
info["packageStatus"] = output.PackageStatus
|
||
info["effectiveStatus"] = output.EffectiveStatus
|
||
if output.Customer != nil {
|
||
info["customerName"] = output.Customer.DeptName
|
||
}
|
||
if output.Package != nil {
|
||
info["packageName"] = output.Package.PackageName
|
||
info["packageType"] = output.Package.PackageType
|
||
}
|
||
}
|
||
return gjson.New(info)
|
||
}
|
||
|
||
func (p *Processor) validateExecuteContext(ctx context.Context, tx gdb.TX, in *model.TSimCardOutboundSubmitParam, operator batchimport.OperatorContext, rows []*Row, requiredSourceBatchNo string) error {
|
||
normalizeSubmitParam(in)
|
||
if err := validateSubmitParam(in); err != nil {
|
||
return err
|
||
}
|
||
if err := p.validateSelectablePackage(ctx, tx, in.PackageCode, operator.TenantId); err != nil {
|
||
return err
|
||
}
|
||
iccids, err := validateOutboundIdentifiers(rows)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(iccids) == 0 {
|
||
return errOutboundValidationRejected
|
||
}
|
||
stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids)
|
||
if err != nil {
|
||
return gerror.Wrap(err, "校验出库库存卡失败")
|
||
}
|
||
formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids)
|
||
if err != nil {
|
||
return gerror.Wrap(err, "校验正式卡失败")
|
||
}
|
||
|
||
for _, row := range rows {
|
||
if row == nil || row.Reason != "" {
|
||
continue
|
||
}
|
||
stock := stockByIccid[row.Iccid]
|
||
reason := validateOutboundRow(row, stock, formalByIccid[row.Iccid], operator, p.opts.StrictStockIdentity, requiredSourceBatchNo)
|
||
if reason != "" {
|
||
row.AddReason(reason)
|
||
}
|
||
}
|
||
|
||
if hasInvalidRows(rows) {
|
||
markRemainingRowsSkipped(rows)
|
||
return errOutboundValidationRejected
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOutboundIdentifiers(rows []*Row) ([]string, error) {
|
||
seen := make(map[string]*Row, len(rows))
|
||
for _, row := range rows {
|
||
if row == nil {
|
||
return nil, gerror.New("出库行不能为空")
|
||
}
|
||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||
if row.Iccid == "" {
|
||
row.AddReason("ICCID不能为空")
|
||
continue
|
||
}
|
||
if first, exists := seen[row.Iccid]; exists {
|
||
row.AddReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo))
|
||
first.AddReason(fmt.Sprintf("ICCID与第%d行重复", row.RowNo))
|
||
continue
|
||
}
|
||
seen[row.Iccid] = row
|
||
}
|
||
iccids := make([]string, 0, len(seen))
|
||
for iccid, row := range seen {
|
||
if row.Reason == "" {
|
||
iccids = append(iccids, iccid)
|
||
}
|
||
}
|
||
return iccids, nil
|
||
}
|
||
|
||
func (p *Processor) validateSelectablePackage(ctx context.Context, tx gdb.TX, packageCode, tenantId string) error {
|
||
pkg, err := p.repo.LoadPackage(ctx, tx, tenantId, packageCode)
|
||
if err != nil {
|
||
return gerror.Wrap(err, "校验出库套餐失败")
|
||
}
|
||
if pkg == nil {
|
||
return gerror.New("出库套餐不存在")
|
||
}
|
||
if !isOutboundSelectablePackageType(pkg.PackageType) {
|
||
return gerror.New("出库套餐必须是卡套餐或池套餐")
|
||
}
|
||
if pkg.Enabled != 1 {
|
||
return gerror.New("出库套餐未启用,不能出库")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func isOutboundSelectablePackageType(packageType int) bool {
|
||
return packageType == outboundCardPackageType || packageType == outboundPoolPackageType
|
||
}
|
||
|
||
func normalizeSubmitParam(in *model.TSimCardOutboundSubmitParam) {
|
||
if in == nil {
|
||
return
|
||
}
|
||
if in.DurationMonths <= 0 && in.DurationYears > 0 {
|
||
in.DurationMonths = in.DurationYears*12 + in.DurationMonthPart
|
||
}
|
||
if in.DurationMonths <= 0 && in.DurationMonthPart > 0 {
|
||
in.DurationMonths = in.DurationMonthPart
|
||
}
|
||
if in.DurationYears <= 0 && in.DurationMonthPart <= 0 && in.DurationMonths > 0 {
|
||
in.DurationYears = in.DurationMonths / 12
|
||
in.DurationMonthPart = in.DurationMonths % 12
|
||
}
|
||
if in.GiftDurationMonths <= 0 && in.GiftDurationYears > 0 {
|
||
in.GiftDurationMonths = in.GiftDurationYears*12 + in.GiftDurationMonthPart
|
||
}
|
||
if in.GiftDurationMonths <= 0 && in.GiftDurationMonthPart > 0 {
|
||
in.GiftDurationMonths = in.GiftDurationMonthPart
|
||
}
|
||
if in.GiftDurationYears <= 0 && in.GiftDurationMonthPart <= 0 && in.GiftDurationMonths > 0 {
|
||
in.GiftDurationYears = in.GiftDurationMonths / 12
|
||
in.GiftDurationMonthPart = in.GiftDurationMonths % 12
|
||
}
|
||
disablePeriodOptions(in)
|
||
if remark := normalizedRemark(in); remark != "" {
|
||
in.Remark = remark
|
||
in.BuyerNote = remark
|
||
in.AdminNote = remark
|
||
}
|
||
}
|
||
|
||
func disablePeriodOptions(in *model.TSimCardOutboundSubmitParam) {
|
||
if in == nil {
|
||
return
|
||
}
|
||
// 1 表示否。出库不再暴露测试期、沉默期配置,统一按无测试期/无沉默期保存。
|
||
in.HasTestPeriod = 1
|
||
in.TestPeriodDays = 0
|
||
in.TestPeriodLatestExpiryDate = nil
|
||
in.HasSilentPeriod = 1
|
||
in.SilentPeriodDays = 0
|
||
in.SilentPeriodLatestExpiryDate = nil
|
||
}
|
||
|
||
func normalizedRemark(in *model.TSimCardOutboundSubmitParam) string {
|
||
if in == nil {
|
||
return ""
|
||
}
|
||
if value := strings.TrimSpace(in.Remark); value != "" {
|
||
return value
|
||
}
|
||
if value := strings.TrimSpace(in.BuyerNote); value != "" {
|
||
return value
|
||
}
|
||
return strings.TrimSpace(in.AdminNote)
|
||
}
|
||
|
||
func (p *Processor) submitParams(params any) *model.TSimCardOutboundSubmitParam {
|
||
if in := outboundSubmitParams(params); in != nil {
|
||
return in
|
||
}
|
||
return p.excelSubmitParam
|
||
}
|
||
|
||
func validateOutboundRow(row *Row, stock *entity.TSimCardStock, formalExists bool, operator batchimport.OperatorContext, strictStockIdentity bool, requiredSourceBatchNo string) string {
|
||
var reasons []string
|
||
if stock == nil {
|
||
return "ICCID不存在库存记录,不能出库"
|
||
}
|
||
if stock.CreatedDept != operator.DeptId {
|
||
reasons = append(reasons, "ICCID不属于当前机构库存,不能出库")
|
||
}
|
||
if requiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != requiredSourceBatchNo {
|
||
reasons = append(reasons, "ICCID不属于指定库存批次,不能出库")
|
||
}
|
||
if formalExists {
|
||
reasons = append(reasons, "ICCID已存在正式卡记录,不能出库")
|
||
}
|
||
if strictStockIdentity {
|
||
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
|
||
reasons = append(reasons, "MSISDN与库存记录不一致,不能出库")
|
||
}
|
||
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
|
||
reasons = append(reasons, "IMSI与库存记录不一致,不能出库")
|
||
}
|
||
}
|
||
return strings.Join(reasons, ";")
|
||
}
|
||
|
||
func hasInvalidRows(rows []*Row) bool {
|
||
for _, row := range rows {
|
||
if row != nil && row.Reason != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func markRemainingRowsSkipped(rows []*Row) {
|
||
for _, row := range rows {
|
||
if row != nil && row.Reason == "" {
|
||
row.AddReason(outboundNotExecutedReason)
|
||
}
|
||
}
|
||
}
|
||
|
||
func outboundSubmitParams(params any) *model.TSimCardOutboundSubmitParam {
|
||
switch in := params.(type) {
|
||
case *model.TSimCardOutboundSubmitParam:
|
||
return in
|
||
case *model.TSimCardOutboundRangeSubmitParam:
|
||
if in == nil {
|
||
return nil
|
||
}
|
||
return &in.TSimCardOutboundSubmitParam
|
||
case *model.TSimCardOutboundInputSubmitParam:
|
||
if in == nil {
|
||
return nil
|
||
}
|
||
return &in.TSimCardOutboundSubmitParam
|
||
case *model.TSimCardOutboundBatchSubmitParam:
|
||
if in == nil {
|
||
return nil
|
||
}
|
||
return &in.TSimCardOutboundSubmitParam
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func requiredSourceBatchNo(params any) string {
|
||
if in, ok := params.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil {
|
||
return strings.TrimSpace(in.BatchNo)
|
||
}
|
||
return ""
|
||
}
|