Files
iotlcm/server/internal/logic/iot/simcardbatch/outbound/processor.go
T
MiFaZhan 59eb34976a chore: 初始开源发布
iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
2026-10-23 14:48:30 +08:00

402 lines
12 KiB
Go

package outbound
import (
"context"
"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 := strictRowsToIccids(rows)
if err != nil {
return err
}
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 gerror.New("存在不满足出库条件的卡,本次未执行")
}
return 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 {
if stock == nil {
return "ICCID不存在库存记录,不能出库"
}
if stock.CreatedDept != operator.DeptId {
return "ICCID不属于当前机构库存,不能出库"
}
if requiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != requiredSourceBatchNo {
return "ICCID不属于指定库存批次,不能出库"
}
if formalExists {
return "ICCID已存在正式卡记录,不能出库"
}
if strictStockIdentity {
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
return "MSISDN与库存记录不一致,不能出库"
}
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
return "IMSI与库存记录不一致,不能出库"
}
}
return ""
}
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("同批存在不满足出库条件的卡,本行未执行")
}
}
}
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 ""
}