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,28 @@
|
||||
package inbound
|
||||
|
||||
func TemplateHeaders() []string {
|
||||
return inboundExcelTemplateHeaders()
|
||||
}
|
||||
|
||||
func inboundExcelTemplateHeaders() []string {
|
||||
return []string{
|
||||
"ICCID*",
|
||||
"MSISDN*",
|
||||
"IMSI*",
|
||||
"IMEI",
|
||||
"通道编号*",
|
||||
"卡类型",
|
||||
"上游套餐编码",
|
||||
"供应商编码",
|
||||
"上游激活时间",
|
||||
"是否贴片转插拔",
|
||||
"是否运营商达量断网",
|
||||
"协议到期时间",
|
||||
"是否有测试期",
|
||||
"测试期天数",
|
||||
"测试期最晚过期日期",
|
||||
"是否有沉默期",
|
||||
"沉默期天数",
|
||||
"沉默期最晚过期日期",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"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"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
const inputInlineMaxRowsConfigKey = "batch.inbound.inputInlineMaxRows"
|
||||
|
||||
type BatchExecuteMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
BatchType int `json:"batchType"`
|
||||
}
|
||||
|
||||
type PendingResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
}
|
||||
|
||||
func CreatePendingExcel(ctx context.Context, file *ghttp.UploadFile, in *model.TSimCardInboundExcelSubmitParam) (*PendingResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardInboundExcelSubmitParam{}
|
||||
}
|
||||
if file == nil {
|
||||
return nil, gerror.New("请选择上传文件")
|
||||
}
|
||||
if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Size > consts.SimCardInboundMaxFileSize {
|
||||
return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", consts.SimCardInboundMaxFileSize>>20)
|
||||
}
|
||||
|
||||
batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := readUploadContent(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: batchNo + "-import.xlsx",
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: consts.SimCardInboundSubDirImportExcel,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存入库原始Excel失败")
|
||||
}
|
||||
|
||||
info := newPendingBatchInfo()
|
||||
info["source"] = g.Map{"importFileOssId": importOss.OssId}
|
||||
info["inboundOptions"] = inboundOptions(in.InboundCommon())
|
||||
return createPendingAndPublish(ctx, pendingInput{
|
||||
BatchNo: batchNo,
|
||||
SourceType: consts.BatchSourceTypeFile,
|
||||
SourceOssId: importOss.OssId,
|
||||
BatchInfo: gjson.New(info),
|
||||
})
|
||||
}
|
||||
|
||||
func CreatePendingRange(ctx context.Context, in *model.TSimCardInboundRangeSubmitParam) (*PendingResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardInboundRangeSubmitParam{}
|
||||
}
|
||||
if err := validateInboundCommon(in.InboundCommon()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rangeInfo, err := pendingRangeInfo(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := newPendingBatchInfo()
|
||||
info["source"] = g.Map{"range": rangeInfo}
|
||||
info["inboundOptions"] = inboundOptions(in.InboundCommon())
|
||||
return createPendingAndPublish(ctx, pendingInput{
|
||||
BatchNo: batchNo,
|
||||
SourceType: consts.BatchSourceTypeRange,
|
||||
BatchInfo: gjson.New(info),
|
||||
})
|
||||
}
|
||||
|
||||
func CreatePendingInput(ctx context.Context, in *model.TSimCardInboundInputSubmitParam) (*PendingResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardInboundInputSubmitParam{}
|
||||
}
|
||||
if err := validateInboundCommon(in.InboundCommon()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputText := in.Input
|
||||
if strings.TrimSpace(inputText) == "" {
|
||||
return nil, gerror.New("批量输入入库数据为空")
|
||||
}
|
||||
batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceOssId int64
|
||||
source := g.Map{}
|
||||
if countNonEmptyLines(inputText) <= inputInlineMaxRows(ctx) {
|
||||
source["inputText"] = inputText
|
||||
} else {
|
||||
inputOss, saveErr := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: []byte(inputText),
|
||||
FileName: batchNo + "-input.txt",
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: consts.SimCardInboundSubDirImportInput,
|
||||
})
|
||||
if saveErr != nil {
|
||||
return nil, gerror.Wrap(saveErr, "保存入库批量输入原文失败")
|
||||
}
|
||||
source["inputFileOssId"] = inputOss.OssId
|
||||
sourceOssId = inputOss.OssId
|
||||
}
|
||||
|
||||
info := newPendingBatchInfo()
|
||||
info["source"] = source
|
||||
info["inboundOptions"] = inboundOptions(in.InboundCommon())
|
||||
return createPendingAndPublish(ctx, pendingInput{
|
||||
BatchNo: batchNo,
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
SourceOssId: sourceOssId,
|
||||
BatchInfo: gjson.New(info),
|
||||
})
|
||||
}
|
||||
|
||||
type pendingInput struct {
|
||||
BatchNo string
|
||||
SourceType int
|
||||
SourceOssId int64
|
||||
BatchInfo *gjson.Json
|
||||
}
|
||||
|
||||
func createPendingAndPublish(ctx context.Context, input pendingInput) (*PendingResult, error) {
|
||||
if strings.TrimSpace(input.BatchNo) == "" {
|
||||
return nil, gerror.New("批次号为空")
|
||||
}
|
||||
operator := batchimport.OperatorContext{
|
||||
TenantId: contexts.GetTenantId(ctx),
|
||||
DeptId: contexts.GetDeptId(ctx),
|
||||
UserId: contexts.GetUserId(ctx),
|
||||
}
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, insertErr := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(do.BatchMain{
|
||||
BatchNo: input.BatchNo,
|
||||
OrderSn: "",
|
||||
BatchType: consts.BatchTypeInbound,
|
||||
BatchStatus: consts.BatchStatusNormal,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
SourceType: input.SourceType,
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
SourceBatchNo: input.BatchNo,
|
||||
SourceOssId: inboundOssIDValue(input.SourceOssId),
|
||||
IsCrossDept: 0,
|
||||
DeptId: operator.DeptId,
|
||||
TotalCount: 0,
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
FailReason: "",
|
||||
BatchInfo: input.BatchInfo,
|
||||
TenantId: operator.TenantId,
|
||||
CreatedDept: operator.DeptId,
|
||||
CreatedBy: operator.UserId,
|
||||
UpdatedBy: operator.UserId,
|
||||
}).Insert(); insertErr != nil {
|
||||
return gerror.Wrap(insertErr, "写入待处理入库批次失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishBatch(ctx, input.BatchNo)
|
||||
return &PendingResult{BatchNo: input.BatchNo, BatchType: consts.BatchTypeInbound}, nil
|
||||
}
|
||||
|
||||
func publishBatch(ctx context.Context, batchNo string) {
|
||||
if err := rabbitmq.Publish(ctx, consts.QueueBatchExecute, BatchExecuteMessage{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeInbound,
|
||||
}); err != nil {
|
||||
g.Log().Errorf(ctx, "inbound batch async publish failed, batchNo=%s, batchType=%d, tenantId=%s, error=%+v",
|
||||
batchNo, consts.BatchTypeInbound, contexts.GetTenantId(ctx), err)
|
||||
}
|
||||
}
|
||||
|
||||
func inboundOssIDValue(ossID int64) any {
|
||||
if ossID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return ossID
|
||||
}
|
||||
|
||||
func newPendingBatchInfo() g.Map {
|
||||
return g.Map{
|
||||
"resultFile": g.Map{
|
||||
"status": "pending",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func inboundOptions(common model.TSimCardInboundCommonFields) g.Map {
|
||||
return g.Map{
|
||||
"chanKey": strings.TrimSpace(common.ChanKey),
|
||||
"validateUpstreamBasicInfo": common.ValidateUpstreamBasicInfo,
|
||||
"cardType": strings.TrimSpace(common.CardType),
|
||||
"upPkgCode": strings.TrimSpace(common.UpPkgCode),
|
||||
"suppliersCode": strings.TrimSpace(common.SuppliersCode),
|
||||
"upstreamActivationTime": common.UpstreamActivationTime,
|
||||
"insertToPlugEnabled": common.InsertToPlugEnabled,
|
||||
"carrierTrafficCutoffEnabled": common.CarrierTrafficCutoffEnabled,
|
||||
"agreementExpiryDate": common.AgreementExpiryDate,
|
||||
"hasTestPeriod": common.HasTestPeriod,
|
||||
"testPeriodDays": common.TestPeriodDays,
|
||||
"testPeriodLatestExpiryDate": common.TestPeriodLatestExpiryDate,
|
||||
"hasSilentPeriod": common.HasSilentPeriod,
|
||||
"silentPeriodDays": common.SilentPeriodDays,
|
||||
"silentPeriodLatestExpiryDate": common.SilentPeriodLatestExpiryDate,
|
||||
}
|
||||
}
|
||||
|
||||
func validateInboundCommon(common model.TSimCardInboundCommonFields) error {
|
||||
if strings.TrimSpace(common.ChanKey) == "" {
|
||||
return gerror.New("通道编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pendingRangeInfo(in *model.TSimCardInboundRangeSubmitParam) (g.Map, error) {
|
||||
rangeType := strings.ToLower(strings.TrimSpace(in.RangeType))
|
||||
if rangeType == "" {
|
||||
rangeType = "iccid"
|
||||
}
|
||||
if rangeType != "iccid" && rangeType != "msisdn" && rangeType != "imsi" {
|
||||
return nil, gerror.New("连号类型必须是 iccid、msisdn 或 imsi")
|
||||
}
|
||||
values := g.Map{
|
||||
"rangeType": rangeType,
|
||||
"startIccid": strings.TrimSpace(in.StartIccid),
|
||||
"endIccid": strings.TrimSpace(in.EndIccid),
|
||||
"startMsisdn": strings.TrimSpace(in.StartMsisdn),
|
||||
"endMsisdn": strings.TrimSpace(in.EndMsisdn),
|
||||
"startImsi": strings.TrimSpace(in.StartImsi),
|
||||
"endImsi": strings.TrimSpace(in.EndImsi),
|
||||
}
|
||||
fieldNames := map[string][2]string{
|
||||
"iccid": {"startIccid", "endIccid"},
|
||||
"msisdn": {"startMsisdn", "endMsisdn"},
|
||||
"imsi": {"startImsi", "endImsi"},
|
||||
}
|
||||
for _, key := range fieldNames[rangeType] {
|
||||
if strings.TrimSpace(values[key].(string)) == "" {
|
||||
return nil, gerror.Newf("%s不能为空", key)
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func countNonEmptyLines(input string) int {
|
||||
count := 0
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func inputInlineMaxRows(ctx context.Context) int {
|
||||
value := g.Cfg().MustGet(ctx, inputInlineMaxRowsConfigKey, consts.SimCardInboundDefaultInputInlineMaxRows).Int()
|
||||
if value <= 0 {
|
||||
return consts.SimCardInboundDefaultInputInlineMaxRows
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func readUploadContent(file *ghttp.UploadFile) ([]byte, error) {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "打开上传文件失败")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取上传文件失败")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
Reference in New Issue
Block a user