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:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
@@ -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
}