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 已说明。
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user