59eb34976a
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 已说明。
402 lines
12 KiB
Go
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 ""
|
|
}
|