chore: 初始开源发布
iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。
- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestSaveBatchMainExecutionMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE TABLE batch_main (id INTEGER PRIMARY KEY AUTOINCREMENT, batch_no TEXT, tenant_id TEXT, dept_id INTEGER, source_channel TEXT, execution_mode INTEGER)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, mode := range []int{batchmetadata.Single, batchmetadata.Bulk} {
|
||||
batchNo := "SINGLE"
|
||||
if mode == batchmetadata.Bulk {
|
||||
batchNo = "BULK"
|
||||
}
|
||||
if err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return New().SaveBatchMain(ctx, tx, &entity.BatchMain{BatchNo: batchNo, TenantId: "A", DeptId: 7}, mode)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
record, err := g.DB().GetOne(ctx, "SELECT * FROM batch_main WHERE batch_no = ?", batchNo)
|
||||
if err != nil || record["source_channel"].String() != "api" || record["execution_mode"].Int() != mode || record["tenant_id"].String() != "A" || record["dept_id"].Int() != 7 {
|
||||
t.Fatalf("metadata not persisted: record=%+v err=%v", record, err)
|
||||
}
|
||||
}
|
||||
for _, mode := range []int{-1, batchmetadata.Unknown, 3} {
|
||||
if err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return New().SaveBatchMain(ctx, tx, &entity.BatchMain{BatchNo: "INVALID"}, mode)
|
||||
}); err == nil {
|
||||
t.Fatalf("invalid creation mode accepted: %d", mode)
|
||||
}
|
||||
}
|
||||
count, err := g.DB().GetValue(ctx, "SELECT COUNT(*) FROM batch_main")
|
||||
if err != nil || count.Int() != 2 {
|
||||
t.Fatalf("invalid batch inserted: count=%v err=%v", count, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
const (
|
||||
singleBatchWaitTimeout = 60 * time.Second
|
||||
singleBatchPollInterval = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func (s *sCommon) waitBatchCompletion(ctx context.Context, tenantID string, deptID int64, batchNo string) (int, error) {
|
||||
deadline := time.Now().Add(singleBatchWaitTimeout)
|
||||
for {
|
||||
status, err := s.loadBatchAsyncStatus(ctx, tenantID, deptID, batchNo)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch status {
|
||||
case consts.BatchAsyncStatusSuccess, consts.BatchAsyncStatusFailed, consts.BatchAsyncStatusPartialSuccess:
|
||||
return status, nil
|
||||
}
|
||||
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return 0, gerror.Newf("batch %s execution timed out; query batch status for the result", batchNo)
|
||||
}
|
||||
wait := singleBatchPollInterval
|
||||
if remaining < wait {
|
||||
wait = remaining
|
||||
}
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return 0, gerror.Wrap(ctx.Err(), "request canceled while waiting for batch completion")
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sCommon) loadBatchAsyncStatus(ctx context.Context, tenantID string, deptID int64, batchNo string) (int, error) {
|
||||
columns := dao.BatchMain.Columns()
|
||||
var batch entity.BatchMain
|
||||
err := dao.BatchMain.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Scan(&batch)
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "failed to query batch execution status")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return 0, gerror.Newf("batch %s does not exist or is not accessible", batchNo)
|
||||
}
|
||||
return batch.BatchAsyncStatus, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
type cardIdentifier struct {
|
||||
Msisdn string
|
||||
Iccid string
|
||||
Imsi string
|
||||
}
|
||||
|
||||
func validateCardIdentifier(item cardIdentifier) error {
|
||||
count := 0
|
||||
if strings.TrimSpace(item.Msisdn) != "" {
|
||||
count++
|
||||
}
|
||||
if strings.TrimSpace(item.Iccid) != "" {
|
||||
count++
|
||||
}
|
||||
if strings.TrimSpace(item.Imsi) != "" {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("msisdn、iccid、imsi至少传一个")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cardIdentifierKey(item cardIdentifier) string {
|
||||
switch {
|
||||
case item.Iccid != "":
|
||||
return "iccid:" + item.Iccid
|
||||
case item.Msisdn != "":
|
||||
return "msisdn:" + item.Msisdn
|
||||
default:
|
||||
return "imsi:" + item.Imsi
|
||||
}
|
||||
}
|
||||
|
||||
func cardMatchesIdentifier(card *entity.TSimCardInfo, identifier cardIdentifier) bool {
|
||||
if card == nil {
|
||||
return false
|
||||
}
|
||||
if identifier.Iccid != "" && strings.TrimSpace(card.Iccid) != identifier.Iccid {
|
||||
return false
|
||||
}
|
||||
if identifier.Msisdn != "" && strings.TrimSpace(card.Msisdn) != identifier.Msisdn {
|
||||
return false
|
||||
}
|
||||
if identifier.Imsi != "" && strings.TrimSpace(card.Imsi) != identifier.Imsi {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sCommon) loadCardsByIdentifiers(ctx context.Context, tenantID string, deptIDs []int64, identifiers []cardIdentifier) (map[string]*entity.TSimCardInfo, error) {
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
result := make(map[string]*entity.TSimCardInfo, len(identifiers))
|
||||
iccidValues := make([]string, 0)
|
||||
msisdnValues := make([]string, 0)
|
||||
imsiValues := make([]string, 0)
|
||||
for _, item := range identifiers {
|
||||
switch {
|
||||
case item.Iccid != "":
|
||||
iccidValues = append(iccidValues, item.Iccid)
|
||||
case item.Msisdn != "":
|
||||
msisdnValues = append(msisdnValues, item.Msisdn)
|
||||
case item.Imsi != "":
|
||||
imsiValues = append(imsiValues, item.Imsi)
|
||||
}
|
||||
}
|
||||
lookup := func(field string, values []string) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
var rows []*entity.TSimCardInfo
|
||||
query := dao.TSimCardInfo.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
WhereIn(columns.DeptId, deptIDs).
|
||||
WhereIn(field, values).
|
||||
Fields(columns.Iccid, columns.Msisdn, columns.Imsi, columns.PackageTotalFlowMb, columns.UsedFlowMb,
|
||||
columns.CurrentPackageCode, columns.OutboundBatchNo, columns.TrafficCutoffStatus, columns.TrafficCutoffThreshold)
|
||||
if err := query.Scan(&rows); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
if row.Iccid != "" {
|
||||
result["iccid:"+strings.TrimSpace(row.Iccid)] = row
|
||||
}
|
||||
if row.Msisdn != "" {
|
||||
result["msisdn:"+strings.TrimSpace(row.Msisdn)] = row
|
||||
}
|
||||
if row.Imsi != "" {
|
||||
result["imsi:"+strings.TrimSpace(row.Imsi)] = row
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := lookup(columns.Iccid, iccidValues); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡片失败")
|
||||
}
|
||||
if err := lookup(columns.Msisdn, msisdnValues); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡片失败")
|
||||
}
|
||||
if err := lookup(columns.Imsi, imsiValues); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡片失败")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
)
|
||||
|
||||
type sCommon struct{}
|
||||
|
||||
func New() *sCommon {
|
||||
return &sCommon{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommon(New())
|
||||
}
|
||||
|
||||
// BuildBatchNo 与控制台、小程序共用按日递增规则。
|
||||
func (s *sCommon) BuildBatchNo(ctx context.Context, prefix string) (string, error) {
|
||||
return batchimport.NextBatchNoContext(ctx, prefix)
|
||||
}
|
||||
|
||||
// SaveBatchMain 写入批次主表。
|
||||
func (s *sCommon) SaveBatchMain(ctx context.Context, tx gdb.TX, batch *entity.BatchMain, executionMode int) error {
|
||||
if tx == nil {
|
||||
return gerror.New("transaction is empty")
|
||||
}
|
||||
if batch == nil {
|
||||
return gerror.New("batch main is empty")
|
||||
}
|
||||
if strings.TrimSpace(batch.BatchNo) == "" {
|
||||
return gerror.New("batch no is empty")
|
||||
}
|
||||
if executionMode != batchmetadata.Single && executionMode != batchmetadata.Bulk {
|
||||
return gerror.New("batch execution mode is invalid")
|
||||
}
|
||||
data := do.BatchMain{}
|
||||
if err := gconv.Struct(batch, &data); err != nil {
|
||||
return gerror.Wrap(err, "convert batch main failed")
|
||||
}
|
||||
data.SourceChannel = batchmetadata.API
|
||||
data.ExecutionMode = executionMode
|
||||
data.Id = nil
|
||||
data.CreatedAt = nil
|
||||
data.UpdatedAt = nil
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(data).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "save batch main failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveBatchDetail 写入单条批次明细。
|
||||
func (s *sCommon) SaveBatchDetail(ctx context.Context, tx gdb.TX, detail *entity.BatchDetail) error {
|
||||
if tx == nil {
|
||||
return gerror.New("transaction is empty")
|
||||
}
|
||||
if detail == nil {
|
||||
return gerror.New("batch detail is empty")
|
||||
}
|
||||
if strings.TrimSpace(detail.BatchNo) == "" {
|
||||
return gerror.New("batch no is empty")
|
||||
}
|
||||
data := do.BatchDetail{}
|
||||
if err := gconv.Struct(detail, &data); err != nil {
|
||||
return gerror.Wrap(err, "convert batch detail failed")
|
||||
}
|
||||
data.Id = nil
|
||||
data.CreatedAt = nil
|
||||
data.UpdatedAt = nil
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).Data(data).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "save batch detail failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveBatchDetails 批量写入批次明细。为了保持行为稳定,这里按行写入,避免不同数据库驱动对批量结构体的差异。
|
||||
func (s *sCommon) SaveBatchDetails(ctx context.Context, tx gdb.TX, details []*entity.BatchDetail) error {
|
||||
for _, detail := range details {
|
||||
if err := s.SaveBatchDetail(ctx, tx, detail); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sCommon) Publish(ctx context.Context, queue string, data any) error {
|
||||
return rabbitmq.Publish(ctx, queue, data)
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/os/gtime"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
// BuildRenewBatchNo 生成续费类批次号。
|
||||
func (s *sCommon) BuildRenewBatchNo(ctx context.Context) (string, error) {
|
||||
return s.BuildBatchNo(ctx, consts.BatchNoPrefixRenew)
|
||||
}
|
||||
|
||||
// SubmitRenew 创建卡续费批次并投递到 worker 异步执行。
|
||||
func (s *sCommon) SubmitRenew(ctx context.Context, in *model.CommonRenewParam) (*model.CommonRenewRes, error) {
|
||||
return s.submitRenew(ctx, in, false, batchmetadata.Bulk)
|
||||
}
|
||||
|
||||
func (s *sCommon) submitRenew(ctx context.Context, in *model.CommonRenewParam, waitForCompletion bool, executionMode int) (*model.CommonRenewRes, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("续费参数不能为空")
|
||||
}
|
||||
duration := in.DurationYears
|
||||
if duration <= 0 {
|
||||
duration = in.Validity
|
||||
}
|
||||
if duration <= 0 {
|
||||
return nil, gerror.New("续费有效期必须大于0")
|
||||
}
|
||||
packageID := strings.TrimSpace(in.PackageId)
|
||||
cards, err := normalizeRenewCards(in.Cards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tenantID := strings.TrimSpace(contexts.GetTenantId(ctx))
|
||||
deptID := contexts.GetAgentDeptId(ctx)
|
||||
userID := contexts.GetUserId(ctx)
|
||||
if tenantID == "" || deptID <= 0 {
|
||||
return nil, gerror.New("当前接口调用方信息不完整")
|
||||
}
|
||||
if userID <= 0 {
|
||||
userID = deptID
|
||||
}
|
||||
|
||||
deptIDs, err := s.loadRenewAccessibleDeptIds(ctx, tenantID, deptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identifiers := make([]cardIdentifier, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
identifiers = append(identifiers, cardIdentifier{Msisdn: card.Msisdn, Iccid: card.Iccid, Imsi: card.Imsi})
|
||||
}
|
||||
cardMap, err := s.loadCardsByIdentifiers(ctx, tenantID, deptIDs, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index := range cards {
|
||||
dbCard := cardMap[cardIdentifierKey(identifiers[index])]
|
||||
if !cardMatchesIdentifier(dbCard, identifiers[index]) {
|
||||
return nil, gerror.New("卡片不存在或无权限")
|
||||
}
|
||||
cards[index].Iccid = strings.TrimSpace(dbCard.Iccid)
|
||||
cards[index].Msisdn = strings.TrimSpace(dbCard.Msisdn)
|
||||
cards[index].Imsi = strings.TrimSpace(dbCard.Imsi)
|
||||
}
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
iccids = append(iccids, card.Iccid)
|
||||
}
|
||||
|
||||
batchNo, err := s.BuildRenewBatchNo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := gtime.Now()
|
||||
remark := strings.TrimSpace(in.Remark)
|
||||
batchInfo := gjson.New(map[string]any{
|
||||
"durationValue": duration,
|
||||
"durationYears": duration,
|
||||
"durationMonths": duration,
|
||||
"packageId": packageID,
|
||||
"paymentMode": "balance",
|
||||
|
||||
"remark": remark,
|
||||
"quantity": len(cards),
|
||||
"submittedFrom": "openapi",
|
||||
"submittedAt": now.Format("Y-m-d H:i:s"),
|
||||
"runtimeResolveRenewDetail": true,
|
||||
})
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
main := &entity.BatchMain{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
BatchStatus: consts.BatchStatusNormal,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
DeptId: deptID,
|
||||
TotalCount: len(cards),
|
||||
ValidCount: len(cards),
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
BatchInfo: batchInfo,
|
||||
Remark: gjson.New(map[string]any{"remark": remark}),
|
||||
TenantId: tenantID,
|
||||
CreatedDept: deptID,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: userID,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := s.SaveBatchMain(ctx, tx, main, executionMode); err != nil {
|
||||
return err
|
||||
}
|
||||
details := make([]*entity.BatchDetail, 0, len(cards))
|
||||
for i, inputCard := range cards {
|
||||
dbCard := cardMap[cardIdentifierKey(cardIdentifier{Msisdn: inputCard.Msisdn, Iccid: inputCard.Iccid, Imsi: inputCard.Imsi})]
|
||||
snapshot := renewDetailSnapshot(inputCard, dbCard)
|
||||
details = append(details, &entity.BatchDetail{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: i + 1,
|
||||
ItemId: inputCard.Iccid,
|
||||
Snapshot: snapshot,
|
||||
TenantId: tenantID,
|
||||
CreatedDept: deptID,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: userID,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
return s.SaveBatchDetails(ctx, tx, details)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := model.CommonBatchExecuteMessage{BatchNo: batchNo, BatchType: consts.BatchTypeRenew}
|
||||
if err := s.Publish(ctx, consts.QueueBatchExecute, message); err != nil {
|
||||
return nil, gerror.Wrap(err, "投递续费异步消息失败")
|
||||
}
|
||||
if waitForCompletion {
|
||||
if _, err := s.waitBatchCompletion(ctx, tenantID, deptID, batchNo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &model.CommonRenewRes{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
Total: len(cards),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildRenewSubmitRemark(ctx context.Context) string {
|
||||
agent := contexts.GetAgentUser(ctx)
|
||||
if agent == nil {
|
||||
return "OpenAPI续费调用"
|
||||
}
|
||||
deptName := strings.TrimSpace(agent.DeptName)
|
||||
deptKey := strings.TrimSpace(agent.DeptKey)
|
||||
switch {
|
||||
case deptName != "" && deptKey != "":
|
||||
return "OpenAPI续费调用方:" + deptName + "(" + deptKey + ")"
|
||||
case deptName != "":
|
||||
return "OpenAPI续费调用方:" + deptName
|
||||
case deptKey != "":
|
||||
return "OpenAPI续费调用方:" + deptKey
|
||||
default:
|
||||
return "OpenAPI续费调用"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRenewCards(input []model.CommonRenewCard) ([]model.CommonRenewCard, error) {
|
||||
if len(input) == 0 {
|
||||
return nil, gerror.New("续费卡不能为空")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(input))
|
||||
cards := make([]model.CommonRenewCard, 0, len(input))
|
||||
for _, item := range input {
|
||||
item.Iccid = strings.TrimSpace(item.Iccid)
|
||||
item.Msisdn = strings.TrimSpace(item.Msisdn)
|
||||
item.Imsi = strings.TrimSpace(item.Imsi)
|
||||
item.PackageId = strings.TrimSpace(item.PackageId)
|
||||
identifier := cardIdentifier{Msisdn: item.Msisdn, Iccid: item.Iccid, Imsi: item.Imsi}
|
||||
if err := validateCardIdentifier(identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := cardIdentifierKey(identifier)
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, gerror.Newf("卡片标识重复:%s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
cards = append(cards, item)
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
func (s *sCommon) loadRenewAccessibleDeptIds(ctx context.Context, tenantID string, deptID int64) ([]int64, error) {
|
||||
deptIDs := []int64{deptID}
|
||||
columns := dao.SysDept.Columns()
|
||||
array, err := dao.SysDept.Ctx(ctx).
|
||||
Fields(columns.DeptId).
|
||||
Where(columns.TenantId, tenantID).
|
||||
WhereNull(columns.DeletedAt).
|
||||
Where("dept_id = ? OR ancestors LIKE ?", deptID, "%,"+strconv.FormatInt(deptID, 10)+",%").
|
||||
Array()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费数据权限失败")
|
||||
}
|
||||
if len(array) == 0 {
|
||||
return deptIDs, nil
|
||||
}
|
||||
seen := map[int64]struct{}{}
|
||||
deptIDs = deptIDs[:0]
|
||||
for _, item := range array {
|
||||
id := item.Int64()
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
deptIDs = append(deptIDs, id)
|
||||
}
|
||||
if len(deptIDs) == 0 {
|
||||
deptIDs = append(deptIDs, deptID)
|
||||
}
|
||||
return deptIDs, nil
|
||||
}
|
||||
|
||||
func (s *sCommon) loadRenewCards(ctx context.Context, tenantID string, deptIDs []int64, iccids []string) (map[string]*entity.TSimCardInfo, error) {
|
||||
cardMap := make(map[string]*entity.TSimCardInfo, len(iccids))
|
||||
if len(iccids) == 0 {
|
||||
return cardMap, nil
|
||||
}
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
var rows []*entity.TSimCardInfo
|
||||
err := dao.TSimCardInfo.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
WhereIn(columns.DeptId, deptIDs).
|
||||
WhereIn(columns.Iccid, iccids).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费卡失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
if row.Iccid != "" {
|
||||
cardMap[row.Iccid] = row
|
||||
}
|
||||
}
|
||||
return cardMap, nil
|
||||
}
|
||||
|
||||
func renewDetailSnapshot(input model.CommonRenewCard, card *entity.TSimCardInfo) *gjson.Json {
|
||||
data := map[string]any{
|
||||
"iccid": input.Iccid,
|
||||
"targetCode": input.Iccid,
|
||||
}
|
||||
if input.Msisdn != "" {
|
||||
data["msisdn"] = input.Msisdn
|
||||
}
|
||||
if input.Imsi != "" {
|
||||
data["imsi"] = input.Imsi
|
||||
}
|
||||
if input.PackageId != "" {
|
||||
data["packageId"] = input.PackageId
|
||||
data["packageCode"] = input.PackageId
|
||||
}
|
||||
if card != nil {
|
||||
if input.Msisdn == "" {
|
||||
data["msisdn"] = strings.TrimSpace(card.Msisdn)
|
||||
}
|
||||
if input.Imsi == "" {
|
||||
data["imsi"] = strings.TrimSpace(card.Imsi)
|
||||
}
|
||||
if input.PackageId == "" {
|
||||
data["packageCode"] = strings.TrimSpace(card.CurrentPackageCode)
|
||||
}
|
||||
data["currentPackageCode"] = strings.TrimSpace(card.CurrentPackageCode)
|
||||
data["outboundBatchNo"] = strings.TrimSpace(card.OutboundBatchNo)
|
||||
}
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
func (s *sCommon) SubmitSingleRenew(ctx context.Context, card model.CommonRenewCard, appID, packageID, remark string, validity int) (*model.CommonSingleRenewRes, error) {
|
||||
card.PackageId = packageID
|
||||
batch, err := s.submitRenew(ctx, &model.CommonRenewParam{
|
||||
AppId: appID, PackageId: packageID, Validity: validity, DurationYears: validity, Remark: remark,
|
||||
Cards: []model.CommonRenewCard{card},
|
||||
}, true, batchmetadata.Single)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.loadSingleRenewResult(ctx, batch.BatchNo, packageID, validity)
|
||||
}
|
||||
|
||||
func (s *sCommon) SubmitBatchRenew(ctx context.Context, appID string, cards []model.CommonRenewCard, iccids []string, packageID, remark string, validity int) (*model.CommonRenewRes, error) {
|
||||
if len(cards) == 0 {
|
||||
cards = make([]model.CommonRenewCard, 0, len(iccids))
|
||||
for _, iccid := range iccids {
|
||||
cards = append(cards, model.CommonRenewCard{Iccid: iccid, PackageId: packageID})
|
||||
}
|
||||
}
|
||||
for index := range cards {
|
||||
if cards[index].PackageId == "" {
|
||||
cards[index].PackageId = packageID
|
||||
}
|
||||
}
|
||||
return s.SubmitRenew(ctx, &model.CommonRenewParam{
|
||||
AppId: appID, PackageId: packageID, Validity: validity, DurationYears: validity, Remark: remark, Cards: cards,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
func (s *sCommon) loadSingleRenewResult(ctx context.Context, batchNo, packageID string, validity int) (*model.CommonSingleRenewRes, error) {
|
||||
batch, err := s.loadBatchMain(ctx, batchNo, consts.BatchTypeRenew)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detail, err := s.loadSingleBatchDetail(ctx, batchNo, consts.BatchTypeRenew)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &model.CommonSingleRenewRes{
|
||||
OrderId: "0",
|
||||
OrderSn: strings.TrimSpace(batch.OrderSn),
|
||||
Status: "success",
|
||||
}
|
||||
orderSn := result.OrderSn
|
||||
if orderSn != "" {
|
||||
var order entity.TOrder
|
||||
orderColumns := dao.TOrder.Columns()
|
||||
if err := dao.TOrder.Ctx(ctx).
|
||||
Where(orderColumns.TenantId, batch.TenantId).
|
||||
Where(orderColumns.OrderSn, orderSn).
|
||||
Scan(&order); err == nil && order.Id > 0 {
|
||||
result.OrderId = strconv.FormatInt(order.Id, 10)
|
||||
result.Amount = order.TotalAmount
|
||||
}
|
||||
}
|
||||
|
||||
if detail.Snapshot != nil {
|
||||
result.Iccid = firstSnapshotValue(detail.Snapshot, "iccid", "ICCID", "targetCode")
|
||||
result.PlanName = firstSnapshotValue(detail.Snapshot, "packageName")
|
||||
result.EffectiveType = firstSnapshotValue(detail.Snapshot, "effectiveType", "effectiveMode")
|
||||
result.EffectiveDate = firstSnapshotValue(detail.Snapshot, "renewEffectiveTime")
|
||||
result.ExpireDate = firstSnapshotValue(detail.Snapshot, "renewExpectedExpireTime", "cardFlowPkgEndAt")
|
||||
if snapshotAmount := firstSnapshotFloat(detail.Snapshot, "amount", "totalAmount"); snapshotAmount > 0 {
|
||||
result.Amount = snapshotAmount
|
||||
}
|
||||
}
|
||||
if result.EffectiveType == "" {
|
||||
result.EffectiveType = "immediate"
|
||||
}
|
||||
if detail.DetailStatus != consts.BatchDetailStatusSuccess {
|
||||
result.Status = "failed"
|
||||
result.ErrorCode = strings.TrimSpace(detail.ErrorCode)
|
||||
result.ErrorMessage = strings.TrimSpace(detail.ErrorReason)
|
||||
if result.ErrorCode == "" {
|
||||
result.ErrorCode = "RENEW_FAILED"
|
||||
}
|
||||
if result.ErrorMessage == "" {
|
||||
result.ErrorMessage = "续费失败,执行明细未记录具体原因"
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sCommon) loadSingleTrafficCutoffResult(ctx context.Context, batchNo string) (*model.CommonSingleTrafficCutoffRes, error) {
|
||||
detail, err := s.loadSingleBatchDetail(ctx, batchNo, consts.BatchTypeTrafficCutoffConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &model.CommonSingleTrafficCutoffRes{
|
||||
Success: detail.DetailStatus == consts.BatchDetailStatusSuccess,
|
||||
Iccid: strings.TrimSpace(detail.ItemId),
|
||||
Message: "traffic cutoff configuration succeeded",
|
||||
ErrorCode: strings.TrimSpace(detail.ErrorCode),
|
||||
}
|
||||
if detail.Snapshot != nil {
|
||||
result.Iccid = firstSnapshotValue(detail.Snapshot, "iccid", "ICCID")
|
||||
result.Msisdn = firstSnapshotValue(detail.Snapshot, "msisdn", "MSISDN")
|
||||
result.Imsi = firstSnapshotValue(detail.Snapshot, "imsi", "IMSI")
|
||||
result.TrafficCutoffStatus = firstSnapshotInt(detail.Snapshot, "trafficCutoffStatusAfter", "trafficCutoffStatus")
|
||||
result.TrafficCutoffThreshold = firstSnapshotFloat(detail.Snapshot, "trafficCutoffThresholdAfter", "trafficCutoffThreshold")
|
||||
result.TrafficCutoffThresholdUnit = firstSnapshotValue(detail.Snapshot, "thresholdUnit", "trafficCutoffThresholdUnit")
|
||||
}
|
||||
if result.TrafficCutoffStatus == 0 {
|
||||
result.TrafficCutoffThreshold = 0
|
||||
}
|
||||
if result.TrafficCutoffThresholdUnit == "" {
|
||||
result.TrafficCutoffThresholdUnit = "MB"
|
||||
}
|
||||
if !result.Success {
|
||||
result.Message = strings.TrimSpace(detail.ErrorReason)
|
||||
if result.Message == "" {
|
||||
result.Message = "traffic cutoff configuration failed"
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sCommon) loadSingleBatchDetail(ctx context.Context, batchNo string, batchType int) (*entity.BatchDetail, error) {
|
||||
tenantID := strings.TrimSpace(contexts.GetTenantId(ctx))
|
||||
deptID := contexts.GetAgentDeptId(ctx)
|
||||
if tenantID == "" || deptID <= 0 {
|
||||
return nil, gerror.New("request context is incomplete")
|
||||
}
|
||||
columns := dao.BatchDetail.Columns()
|
||||
var detail entity.BatchDetail
|
||||
err := dao.BatchDetail.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.CreatedDept, deptID).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.BatchType, batchType).
|
||||
Scan(&detail)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "failed to query single-card execution result")
|
||||
}
|
||||
if detail.Id == 0 {
|
||||
return nil, gerror.New("single-card execution result does not exist")
|
||||
}
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
func (s *sCommon) loadBatchMain(ctx context.Context, batchNo string, batchType int) (*entity.BatchMain, error) {
|
||||
tenantID := strings.TrimSpace(contexts.GetTenantId(ctx))
|
||||
deptID := contexts.GetAgentDeptId(ctx)
|
||||
if tenantID == "" || deptID <= 0 {
|
||||
return nil, gerror.New("request context is incomplete")
|
||||
}
|
||||
columns := dao.BatchMain.Columns()
|
||||
var batch entity.BatchMain
|
||||
err := dao.BatchMain.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.CreatedDept, deptID).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.BatchType, batchType).
|
||||
Scan(&batch)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "failed to query batch")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("batch does not exist")
|
||||
}
|
||||
return &batch, nil
|
||||
}
|
||||
|
||||
func firstSnapshotValue(snapshot *gjson.Json, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(snapshot.Get(key).String()); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstSnapshotInt(snapshot *gjson.Json, keys ...string) int {
|
||||
for _, key := range keys {
|
||||
if value := snapshot.Get(key); value != nil && value.String() != "" {
|
||||
return value.Int()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstSnapshotFloat(snapshot *gjson.Json, keys ...string) float64 {
|
||||
for _, key := range keys {
|
||||
if value := snapshot.Get(key); value != nil && value.String() != "" {
|
||||
return value.Float64()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func trafficCutoffStatusName(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "已开启"
|
||||
case 2:
|
||||
return "已关闭"
|
||||
default:
|
||||
return "删除"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"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/os/gtime"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
const (
|
||||
trafficCutoffConfigMaxCards = 1000
|
||||
trafficCutoffRequestIDMaxLen = 64
|
||||
)
|
||||
|
||||
func (s *sCommon) SubmitTrafficCutoffConfig(ctx context.Context, in *model.CommonTrafficCutoffConfigParam) (*model.CommonTrafficCutoffConfigRes, error) {
|
||||
return s.submitTrafficCutoffConfig(ctx, in, false, batchmetadata.Bulk)
|
||||
}
|
||||
|
||||
func (s *sCommon) submitTrafficCutoffConfig(ctx context.Context, in *model.CommonTrafficCutoffConfigParam, waitForCompletion bool, executionMode int) (*model.CommonTrafficCutoffConfigRes, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("达量断网配置参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
idempotencyKey := strings.TrimSpace(in.IdempotencyKey)
|
||||
if len(idempotencyKey) > trafficCutoffRequestIDMaxLen {
|
||||
return nil, gerror.Newf("Idempotency-Key长度不能超过%d", trafficCutoffRequestIDMaxLen)
|
||||
}
|
||||
if in.TrafficCutoffStatus == 0 {
|
||||
in.TrafficCutoffThreshold = 0
|
||||
}
|
||||
if err := validateTrafficCutoffConfig(in.TrafficCutoffStatus, in.TrafficCutoffThreshold); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return nil, gerror.New("配置卡不能为空")
|
||||
}
|
||||
if len(in.Cards) > trafficCutoffConfigMaxCards {
|
||||
return nil, gerror.Newf("单次最多配置%d张卡", trafficCutoffConfigMaxCards)
|
||||
}
|
||||
|
||||
identifiers := make([]cardIdentifier, 0, len(in.Cards))
|
||||
seen := make(map[string]struct{}, len(in.Cards))
|
||||
for _, card := range in.Cards {
|
||||
identifier := cardIdentifier{
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
Iccid: strings.TrimSpace(card.Iccid),
|
||||
Imsi: strings.TrimSpace(card.Imsi),
|
||||
}
|
||||
if err := validateCardIdentifier(identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := cardIdentifierKey(identifier)
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, gerror.Newf("卡片标识重复:%s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
identifiers = append(identifiers, identifier)
|
||||
}
|
||||
|
||||
tenantID := strings.TrimSpace(contexts.GetTenantId(ctx))
|
||||
deptID := contexts.GetAgentDeptId(ctx)
|
||||
if tenantID == "" || deptID <= 0 {
|
||||
return nil, gerror.New("当前接口调用方信息不完整")
|
||||
}
|
||||
operatorID := contexts.GetUserId(ctx)
|
||||
if operatorID <= 0 {
|
||||
operatorID = deptID
|
||||
}
|
||||
|
||||
if idempotencyKey != "" {
|
||||
if existing, err := s.findTrafficCutoffBatch(ctx, tenantID, deptID, idempotencyKey); err != nil {
|
||||
return nil, err
|
||||
} else if existing != nil {
|
||||
return &model.CommonTrafficCutoffConfigRes{BatchNo: existing.BatchNo, BatchType: consts.BatchTypeTrafficCutoffConfig}, nil
|
||||
}
|
||||
}
|
||||
|
||||
deptIDs, err := s.loadRenewAccessibleDeptIds(ctx, tenantID, deptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardMap, err := s.loadCardsByIdentifiers(ctx, tenantID, deptIDs, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iccids := make([]string, 0, len(identifiers))
|
||||
for _, identifier := range identifiers {
|
||||
card := cardMap[cardIdentifierKey(identifier)]
|
||||
if !cardMatchesIdentifier(card, identifier) {
|
||||
return nil, gerror.New("卡片不存在或无权限")
|
||||
}
|
||||
iccids = append(iccids, strings.TrimSpace(card.Iccid))
|
||||
}
|
||||
|
||||
batchNo, err := s.BuildBatchNo(ctx, consts.BatchNoPrefixTrafficCutoffConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := gtime.Now()
|
||||
batchInfo := gjson.New(map[string]any{
|
||||
"operation": "traffic_cutoff_config",
|
||||
"configName": "达量断网配置",
|
||||
"submitScene": "openapi",
|
||||
"trafficCutoffStatus": in.TrafficCutoffStatus,
|
||||
"trafficCutoffThreshold": in.TrafficCutoffThreshold,
|
||||
"trafficCutoffThresholdUnit": "MB",
|
||||
"remark": strings.TrimSpace(in.Remark),
|
||||
"quantity": len(iccids),
|
||||
})
|
||||
if idempotencyKey != "" {
|
||||
batchInfo.Set("idempotencyKey", idempotencyKey)
|
||||
}
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
main := &entity.BatchMain{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeTrafficCutoffConfig,
|
||||
BatchStatus: consts.BatchStatusNormal,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
DeptId: deptID,
|
||||
TotalCount: len(iccids),
|
||||
ValidCount: len(iccids),
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
BatchInfo: batchInfo,
|
||||
Remark: gjson.New(map[string]any{"remark": strings.TrimSpace(in.Remark)}),
|
||||
TenantId: tenantID,
|
||||
CreatedDept: deptID,
|
||||
CreatedBy: operatorID,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: operatorID,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := s.SaveBatchMain(ctx, tx, main, executionMode); err != nil {
|
||||
return err
|
||||
}
|
||||
details := make([]*entity.BatchDetail, 0, len(iccids))
|
||||
for index, iccid := range iccids {
|
||||
card := cardMap[cardIdentifierKey(identifiers[index])]
|
||||
snapshot := gjson.New(map[string]any{
|
||||
"iccid": card.Iccid,
|
||||
"msisdn": card.Msisdn,
|
||||
"imsi": card.Imsi,
|
||||
"authorized": true,
|
||||
"trafficCutoffStatus": in.TrafficCutoffStatus,
|
||||
"trafficCutoffThreshold": in.TrafficCutoffThreshold,
|
||||
"trafficCutoffThresholdUnit": "MB",
|
||||
})
|
||||
details = append(details, &entity.BatchDetail{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeTrafficCutoffConfig,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: index + 1,
|
||||
ItemId: iccid,
|
||||
Snapshot: snapshot,
|
||||
TenantId: tenantID,
|
||||
CreatedDept: deptID,
|
||||
CreatedBy: operatorID,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: operatorID,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
return s.SaveBatchDetails(ctx, tx, details)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := model.CommonBatchExecuteMessage{BatchNo: batchNo, BatchType: consts.BatchTypeTrafficCutoffConfig}
|
||||
if err := s.Publish(ctx, consts.QueueBatchExecute, message); err != nil {
|
||||
return nil, gerror.Wrap(err, "投递达量断网配置异步消息失败")
|
||||
}
|
||||
if waitForCompletion {
|
||||
if _, err := s.waitBatchCompletion(ctx, tenantID, deptID, batchNo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &model.CommonTrafficCutoffConfigRes{BatchNo: batchNo, BatchType: consts.BatchTypeTrafficCutoffConfig}, nil
|
||||
}
|
||||
|
||||
func (s *sCommon) SubmitSingleTrafficCutoffConfig(ctx context.Context, in *model.CommonTrafficCutoffSingleParam) (*model.CommonSingleTrafficCutoffRes, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("traffic cutoff configuration parameters are required")
|
||||
}
|
||||
batch, err := s.submitTrafficCutoffConfig(ctx, &model.CommonTrafficCutoffConfigParam{
|
||||
AppId: in.AppId,
|
||||
Cards: []model.CommonTrafficCutoffCard{{Msisdn: in.Msisdn, Iccid: in.Iccid, Imsi: in.Imsi}},
|
||||
TrafficCutoffStatus: in.TrafficCutoffStatus,
|
||||
TrafficCutoffThreshold: in.TrafficCutoffThreshold,
|
||||
Remark: in.Remark,
|
||||
}, true, batchmetadata.Single)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.loadSingleTrafficCutoffResult(ctx, batch.BatchNo)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
)
|
||||
|
||||
func (s *sCommon) findTrafficCutoffBatch(ctx context.Context, tenantID string, deptID int64, idempotencyKey string) (*entity.BatchMain, error) {
|
||||
var batch entity.BatchMain
|
||||
columns := dao.BatchMain.Columns()
|
||||
err := dao.BatchMain.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.BatchType, consts.BatchTypeTrafficCutoffConfig).
|
||||
Where("JSON_UNQUOTE(JSON_EXTRACT("+columns.BatchInfo+", '$.idempotencyKey')) = ?", idempotencyKey).
|
||||
OrderDesc(columns.Id).
|
||||
Limit(1).
|
||||
Scan(&batch)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询达量断网配置幂等批次失败")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &batch, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func validateTrafficCutoffConfig(status int, threshold float64) error {
|
||||
if status < 0 || status > 2 {
|
||||
return gerror.New("达量断网配置状态无效")
|
||||
}
|
||||
if threshold < 0 {
|
||||
return gerror.New("达量断网阈值不能小于0")
|
||||
}
|
||||
if status == 0 && threshold != 0 {
|
||||
return gerror.New("未配置达量断网时阈值必须为0")
|
||||
}
|
||||
if status == 1 && threshold <= 0 {
|
||||
return gerror.New("开启达量断网时阈值必须大于0")
|
||||
}
|
||||
rounded := math.Round(threshold*1000) / 1000
|
||||
if math.Abs(threshold-rounded) > 0.0000001 {
|
||||
return gerror.New("达量断网阈值最多保留3位小数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user