Files

434 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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 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 ""
}