chore: 并入 iotlcm-openapi 历史到 openapi/(原提交 3b05d5a)
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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/common"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/middleware"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/oldApi"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/openapiiot"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/query"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/upstream"
|
||||
)
|
||||
@@ -0,0 +1,364 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
|
||||
"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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/packed/response"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
)
|
||||
|
||||
type sMiddleware struct{}
|
||||
|
||||
const apiDataScopeRoleId int64 = 17
|
||||
|
||||
func New() *sMiddleware {
|
||||
return &sMiddleware{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterMiddleware(New())
|
||||
}
|
||||
|
||||
func getContentType(r *ghttp.Request) (contentType string) {
|
||||
contentType = r.Response.Header().Get("Content-Type")
|
||||
if contentType != "" {
|
||||
return
|
||||
}
|
||||
|
||||
mime := gmeta.Get(r.GetHandlerResponse(), "mime").String()
|
||||
if mime == "" {
|
||||
contentType = consts.HTTPContentTypeJson
|
||||
} else {
|
||||
contentType = mime
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *sMiddleware) ResponseHandler(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
if r.Response.BufferLength() > 0 || contexts.IsWebSocket(r.GetCtx()) {
|
||||
return
|
||||
}
|
||||
if getContentType(r) == consts.HTTPContentTypeExcel {
|
||||
r.Response.Status = http.StatusOK
|
||||
return
|
||||
}
|
||||
|
||||
err := r.GetError()
|
||||
res := r.GetHandlerResponse()
|
||||
var code gcode.Code = gcode.CodeOK
|
||||
if err != nil {
|
||||
code = gerror.Code(err)
|
||||
if code == gcode.CodeNil {
|
||||
code = gcode.CodeInternalError
|
||||
}
|
||||
response.JsonExit(r, code.Code(), err.Error())
|
||||
return
|
||||
}
|
||||
response.Json(r, code.Code(), "", res)
|
||||
}
|
||||
|
||||
func (s *sMiddleware) ApiCtx(r *ghttp.Request) {
|
||||
ctx := r.GetCtx()
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
if r.URL != nil && s.IsExceptLogin(ctx, r.URL.Path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
data, err := service.QueryToken().GetCurrentCustomer(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.ApiCtx current customer error: %v", err)
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
if data != nil {
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: 0,
|
||||
TenantId: data.BaseClaimsApi.TenantId,
|
||||
DeptId: data.BaseClaimsApi.DeptId,
|
||||
Roles: []model.Role{
|
||||
{RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow},
|
||||
},
|
||||
LoginAt: data.BaseClaimsApi.LoginAt,
|
||||
},
|
||||
}
|
||||
contextModel.AgentUser = &data.BaseClaimsApi
|
||||
contexts.Init(r, contextModel)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func (s *sMiddleware) ApiAuth(r *ghttp.Request) {
|
||||
ctx := r.Context()
|
||||
path := r.URL.Path
|
||||
if s.IsExceptLogin(ctx, path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
deptKey := contexts.GetDeptKey(ctx)
|
||||
if deptKey == "" {
|
||||
g.Log().Error(ctx, "sMiddleware.ApiAuth deptKey is empty", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
|
||||
appId := r.Get("appId").String()
|
||||
if appId == "" {
|
||||
g.Log().Error(ctx, "sMiddleware.ApiAuth appId is empty", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if deptKey != appId {
|
||||
g.Log().Error(ctx, "sMiddleware.ApiAuth deptKey not match appId", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if contexts.GetAgentDeptId(ctx) == 0 {
|
||||
g.Log().Error(ctx, "sMiddleware.ApiAuth deptId is 0", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
allowed, err := s.hasOpenApiPermission(ctx, r.Method, path)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.ApiAuth permission check failed, method: %s, path: %s, err: %v", r.Method, path, err)
|
||||
response.JsonExit(r, consts.CodePermissionCheckFailed.Code(), consts.CodePermissionCheckFailed.Message())
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
|
||||
return
|
||||
}
|
||||
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func openApiMethodValues(method string) []string {
|
||||
method = strings.ToUpper(strings.TrimSpace(method))
|
||||
values := []string{method}
|
||||
switch method {
|
||||
case "GET":
|
||||
values = append(values, "4")
|
||||
case "POST":
|
||||
values = append(values, "1")
|
||||
case "PUT":
|
||||
values = append(values, "2")
|
||||
case "DELETE":
|
||||
values = append(values, "3")
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// hasOpenApiPermission 判定当前 API 账号对指定路由是否已授权。
|
||||
// 返回值 error 仅代表权限数据查询本身的系统故障,与「无权限」(false, nil) 严格区分:
|
||||
// 调用方对 error 返回系统异常码,对 false 返回无权限码,避免把数据库故障误报成授权问题。
|
||||
// 未登记资源、资源停用、账号不可用、未绑定权限组均按无权限处理。
|
||||
func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path string) (bool, error) {
|
||||
agentUser := contexts.GetAgentUser(ctx)
|
||||
if agentUser == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
path = strings.TrimSpace(path)
|
||||
if path != "/" {
|
||||
path = strings.TrimRight(path, "/")
|
||||
}
|
||||
// 每条受保护路由(含 /common 兼容前缀的别名路由)都必须显式登记资源并完成授权,
|
||||
// 资源查询失败不得降级为放行。
|
||||
var apiResource entity.TApiResource
|
||||
err := dao.TApiResource.Ctx(ctx).
|
||||
WhereIn(dao.TApiResource.Columns().Method, openApiMethodValues(method)).
|
||||
Where(dao.TApiResource.Columns().Path, path).
|
||||
Scan(&apiResource)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return false, gerror.Wrapf(err, "query t_api_resource failed, method: %s, path: %s", method, path)
|
||||
}
|
||||
if err != nil || apiResource.Id == 0 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission resource not configured, method: %s, path: %s", method, path)
|
||||
return false, nil
|
||||
}
|
||||
if apiResource.Status == 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var tApi entity.TApi
|
||||
apiMod := dao.TApi.Ctx(ctx)
|
||||
if agentUser.ApiId > 0 {
|
||||
apiMod = apiMod.Where(dao.TApi.Columns().Id, agentUser.ApiId)
|
||||
} else if agentUser.AppKey != "" {
|
||||
apiMod = apiMod.Where(dao.TApi.Columns().AppKey, agentUser.AppKey)
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission api identity is empty, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
return false, nil
|
||||
}
|
||||
if err := apiMod.Scan(&tApi); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api not found, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
return false, nil
|
||||
}
|
||||
return false, gerror.Wrapf(err, "query t_api failed, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
}
|
||||
if tApi.Id == 0 || tApi.Status == 1 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api unavailable, tApiId: %d, status: %d, resourceApiId: %d, path: %s", tApi.Id, tApi.Status, apiResource.Id, path)
|
||||
return false, nil
|
||||
}
|
||||
if tApi.GroupId <= 0 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission groupId is empty, tApiId: %d, appKey: %s, resourceApiId: %d, path: %s", tApi.Id, tApi.AppKey, apiResource.Id, path)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
count, err := dao.TApiGroupResource.Ctx(ctx).
|
||||
Where(dao.TApiGroupResource.Columns().GroupId, tApi.GroupId).
|
||||
Where(dao.TApiGroupResource.Columns().ApiId, apiResource.Id).
|
||||
Count()
|
||||
if err != nil {
|
||||
return false, gerror.Wrapf(err, "query t_api_group_resource failed, groupId: %d, resourceApiId: %d, path: %s", tApi.GroupId, apiResource.Id, path)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (m *sMiddleware) OldResponseHandler(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
if r.Response.BufferLength() > 0 || contexts.IsWebSocket(r.GetCtx()) {
|
||||
return
|
||||
}
|
||||
if getContentType(r) == consts.HTTPContentTypeExcel {
|
||||
r.Response.Status = http.StatusOK
|
||||
return
|
||||
}
|
||||
|
||||
err := r.GetError()
|
||||
res := r.GetHandlerResponse()
|
||||
var code gcode.Code = gcode.CodeOK
|
||||
if err != nil {
|
||||
code = gerror.Code(err)
|
||||
if code == gcode.CodeNil {
|
||||
code = gcode.CodeInternalError
|
||||
}
|
||||
response.OldJson(r, code.Code(), err.Error())
|
||||
return
|
||||
}
|
||||
response.OldJson(r, code.Code(), "", res)
|
||||
}
|
||||
|
||||
func (s *sMiddleware) OldCtx(r *ghttp.Request) {
|
||||
ctx := r.GetCtx()
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
|
||||
verifyVar := r.Get("verify")
|
||||
var verify model.VerifyItem
|
||||
if err := verifyVar.Scan(&verify); err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.OldCtx verify scan error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid")
|
||||
return
|
||||
}
|
||||
|
||||
var tApi entity.TApi
|
||||
err := dao.TApi.Ctx(ctx).Where(g.Map{
|
||||
dao.TApi.Columns().AppKey: verify.AppId,
|
||||
}).Scan(&tApi)
|
||||
if err != nil {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
if tApi.Id != 0 {
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: 0,
|
||||
TenantId: tApi.TenantId,
|
||||
DeptId: tApi.DeptId,
|
||||
Roles: []model.Role{
|
||||
{RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow},
|
||||
},
|
||||
LoginAt: gtime.Now().TimestampMilli(),
|
||||
},
|
||||
}
|
||||
contextModel.AgentUser = &model.BaseClaimsApi{
|
||||
ApiId: tApi.Id,
|
||||
AppKey: tApi.AppKey,
|
||||
GroupId: tApi.GroupId,
|
||||
DeptId: tApi.DeptId,
|
||||
TenantId: tApi.TenantId,
|
||||
LoginAt: gtime.Now().TimestampMilli(),
|
||||
}
|
||||
contexts.Init(r, contextModel)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func (s *sMiddleware) OldAuth(r *ghttp.Request) {
|
||||
ctx := r.Context()
|
||||
path := r.URL.Path
|
||||
if s.IsExceptLogin(ctx, path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
verifyVar := r.Get("verify")
|
||||
var verify model.VerifyItem
|
||||
if err := verifyVar.Scan(&verify); err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.OldAuth verify scan error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid")
|
||||
return
|
||||
}
|
||||
if verify.TimeStamp == "" {
|
||||
g.Log().Error(ctx, "sMiddleware.OldAuth timestamp is empty", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid")
|
||||
return
|
||||
}
|
||||
|
||||
givenTime, err := strconv.ParseInt(verify.TimeStamp, 10, 64)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.OldAuth timestamp parse error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid")
|
||||
return
|
||||
}
|
||||
if givenTime > gtime.Now().TimestampMilli()+3*60*1000 {
|
||||
g.Log().Error(ctx, "sMiddleware.OldAuth timestamp expired", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request timeout")
|
||||
return
|
||||
}
|
||||
if verify.AppId == "" {
|
||||
g.Log().Error(ctx, "sMiddleware.OldAuth appId is empty", "path", path)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid")
|
||||
return
|
||||
}
|
||||
|
||||
var tApi entity.TApi
|
||||
err = dao.TApi.Ctx(ctx).Where(g.Map{
|
||||
dao.TApi.Columns().AppKey: verify.AppId,
|
||||
}).Scan(&tApi)
|
||||
if err != nil || tApi.Id == 0 || verify.Password != tApi.AppSecret {
|
||||
g.Log().Errorf(ctx, "sMiddleware.OldAuth auth failed: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeUserNotFound.Message())
|
||||
return
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool {
|
||||
pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings()
|
||||
return slices.Contains(pathList, path)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package middleware
|
||||
|
||||
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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
)
|
||||
|
||||
func TestOpenAPIPermission(t *testing.T) {
|
||||
ctx := contexts.Set(context.Background(), &model.Context{
|
||||
AgentUser: &model.BaseClaimsApi{ApiId: 1, AppKey: "test-key"},
|
||||
})
|
||||
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)
|
||||
}
|
||||
exec := func(sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
exec(`CREATE TABLE t_api_resource (id INTEGER PRIMARY KEY, method TEXT, path TEXT, status INTEGER)`)
|
||||
exec(`CREATE TABLE t_api (id INTEGER PRIMARY KEY, app_key TEXT, group_id INTEGER, status INTEGER)`)
|
||||
exec(`CREATE TABLE t_api_group_resource (group_id INTEGER, api_id INTEGER)`)
|
||||
exec(`INSERT INTO t_api VALUES (1, 'test-key', 7, 0)`)
|
||||
exec(`INSERT INTO t_api_resource VALUES
|
||||
(1, '1', '/openapi/v3/change/sim-subscribe', 0),
|
||||
(2, 'POST', '/openapi/v3/query/sim-info', 0),
|
||||
(3, '1', '/openapi/v3/query/sim-status', 1),
|
||||
(4, '1', '/openapi/v3/query/sim-imei-batch', 0)`)
|
||||
exec(`INSERT INTO t_api_group_resource VALUES (7, 1), (7, 2), (7, 3)`)
|
||||
|
||||
middleware := New()
|
||||
check := func(t *testing.T, method, path string) (bool, error) {
|
||||
t.Helper()
|
||||
return middleware.hasOpenApiPermission(ctx, method, path)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, method, path string
|
||||
want bool
|
||||
}{
|
||||
{"authorized numeric method", "POST", "/openapi/v3/change/sim-subscribe", true},
|
||||
{"authorized text method", "POST", "/openapi/v3/query/sim-info", true},
|
||||
{"normalized path", "post", "/openapi/v3/query/sim-info/", true},
|
||||
{"unregistered resource", "POST", "/openapi/v3/query/batch-status", false},
|
||||
{"legacy alias path never inherits grants", "POST", "/openapi/v3/common/sim-subscribe", false},
|
||||
{"wrong method", "GET", "/openapi/v3/change/sim-subscribe", false},
|
||||
{"disabled resource", "POST", "/openapi/v3/query/sim-status", false},
|
||||
{"missing group binding", "POST", "/openapi/v3/query/sim-imei-batch", false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := check(t, tc.method, tc.path)
|
||||
if err != nil {
|
||||
t.Fatalf("business denial must not report a system failure: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("permission = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Token endpoints are exempted by router.exceptLogin only; the middleware must not
|
||||
// carry its own whitelist, otherwise the two lists drift apart.
|
||||
if got, err := check(t, "POST", "/openapi/v3/query/token"); err != nil || got {
|
||||
t.Fatalf("token path must not be whitelisted inside middleware, got=%v err=%v", got, err)
|
||||
}
|
||||
|
||||
// The /common alias prefix has been removed from the router (router/common.go deleted).
|
||||
// Keep the path-level guard anyway: if an alias is ever re-introduced, it must still be
|
||||
// registered and granted on its own full path instead of inheriting /change permissions.
|
||||
exec(`INSERT INTO t_api_resource VALUES (5, '1', '/openapi/v3/common/sim-subscribe', 0)`)
|
||||
if got, err := check(t, "POST", "/openapi/v3/common/sim-subscribe"); err != nil || got {
|
||||
t.Fatalf("registered compatibility route without grant must be denied, got=%v err=%v", got, err)
|
||||
}
|
||||
exec(`INSERT INTO t_api_group_resource VALUES (7, 5)`)
|
||||
if got, err := check(t, "POST", "/openapi/v3/common/sim-subscribe"); err != nil || !got {
|
||||
t.Fatalf("explicitly granted compatibility route must be allowed, got=%v err=%v", got, err)
|
||||
}
|
||||
|
||||
// Unusable accounts are a business denial, not a system failure.
|
||||
for _, sql := range []string{
|
||||
`UPDATE t_api SET status = 1`,
|
||||
`UPDATE t_api SET status = 0, group_id = 0`,
|
||||
`DELETE FROM t_api`,
|
||||
} {
|
||||
exec(sql)
|
||||
got, err := check(t, "POST", "/openapi/v3/query/sim-info")
|
||||
if err != nil {
|
||||
t.Fatalf("account rejection must not be reported as a system failure after %s: %v", sql, err)
|
||||
}
|
||||
if got {
|
||||
t.Fatalf("invalid account allowed after %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// A broken permission lookup is a system failure: it must deny AND surface the error,
|
||||
// so callers can answer 10016 instead of misreporting it as 10014 no-permission.
|
||||
exec(`DROP TABLE t_api_resource`)
|
||||
got, err := check(t, "POST", "/openapi/v3/query/sim-info")
|
||||
if err == nil {
|
||||
t.Fatal("resource lookup failure must return an error")
|
||||
}
|
||||
if got {
|
||||
t.Fatal("resource lookup failure must deny access")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package oldApi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sOldApiQuery struct {
|
||||
}
|
||||
|
||||
func NewOldApiQuery() *sOldApiQuery {
|
||||
return &sOldApiQuery{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterOldApiQuery(NewOldApiQuery())
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) StatusConversion(simCardInfo entity.TSimCardInfo) int {
|
||||
switch simCardInfo.BossCardStatus {
|
||||
case 0:
|
||||
return 8
|
||||
case 1:
|
||||
return 3
|
||||
case 2:
|
||||
return 4
|
||||
case 4:
|
||||
return 5
|
||||
case 6:
|
||||
return 2
|
||||
case 7:
|
||||
return 1
|
||||
case 8:
|
||||
return 6
|
||||
case 9:
|
||||
return 7
|
||||
default:
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
func oldApiDate(times ...*gtime.Time) string {
|
||||
for _, item := range times {
|
||||
if item == nil || item.IsZero() {
|
||||
continue
|
||||
}
|
||||
return item.Format("Y-m-d")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func oldApiEndOfDay(t *gtime.Time) *gtime.Time {
|
||||
if t == nil || t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return t.EndOfDay()
|
||||
}
|
||||
|
||||
func oldApiEndOfDayString(t *gtime.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.EndOfDay().Format("Y-m-d H:i:s")
|
||||
}
|
||||
|
||||
func oldApiScopedCardByIccid(ctx context.Context, iccid string) (card *entity.TSimCardInfo, err error) {
|
||||
err = service.IotTSimCardInfo().
|
||||
Model(ctx).
|
||||
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
|
||||
Limit(1).
|
||||
Scan(&card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
return nil, gerror.New("sim卡不存在")
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QuerySimCard(ctx context.Context, input *model.SimCardInfoInput) (output *model.SimCardInfoOutput, err error) {
|
||||
input.Param.Type = strings.ToLower(strings.TrimSpace(input.Param.Type))
|
||||
input.Param.Cardno = strings.TrimSpace(input.Param.Cardno)
|
||||
if input.Param.Type == "" {
|
||||
return nil, gerror.New("数据类型不能为空")
|
||||
}
|
||||
if input.Param.Cardno == "" {
|
||||
return nil, gerror.New("值不能为空")
|
||||
}
|
||||
|
||||
field := ""
|
||||
if input.Param.Type == "iccid" {
|
||||
field = dao.TSimCardInfo.Columns().Iccid
|
||||
} else if input.Param.Type == "imsi" {
|
||||
field = dao.TSimCardInfo.Columns().Imsi
|
||||
} else if input.Param.Type == "msisdn" {
|
||||
field = dao.TSimCardInfo.Columns().Msisdn
|
||||
} else {
|
||||
return nil, gerror.New("数据类型错误")
|
||||
}
|
||||
|
||||
var out []model.TSimCardInfoListModel
|
||||
sysDept := dao.SysDept.Table()
|
||||
cardInfoTable := dao.TSimCardInfo.Table()
|
||||
flowPackage := dao.TSimFlowPackageInfo.Table()
|
||||
err = service.IotTSimCardInfo().
|
||||
Model(ctx, &handler.Option{FilterAuth: false, FilterTenant: true}).
|
||||
Where(field, input.Param.Cardno).
|
||||
LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().DeptId).
|
||||
LeftJoin(flowPackage, flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode).
|
||||
Fields(cardInfoTable + ".*," + sysDept + "." + dao.SysDept.Columns().DeptName + " as DeptName," + flowPackage + "." + dao.TSimFlowPackageInfo.Columns().PackageName + " as PackageName").
|
||||
Limit(1).
|
||||
Scan(&out)
|
||||
if err != nil {
|
||||
return nil, gerror.New("查询sim卡信息失败")
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, gerror.New("sim卡不存在")
|
||||
}
|
||||
|
||||
output = &model.SimCardInfoOutput{
|
||||
Vid: strconv.FormatInt(out[0].Id, 10),
|
||||
Msisdn: out[0].Msisdn,
|
||||
Iccid: out[0].Iccid,
|
||||
Imei: out[0].Imei,
|
||||
Imsi: out[0].Imsi,
|
||||
DeptName: out[0].DeptName,
|
||||
StatusId: s.StatusConversion(entity.TSimCardInfo{
|
||||
BossCardStatus: out[0].BossCardStatus,
|
||||
}),
|
||||
PackageId: out[0].CurrentPackageCode,
|
||||
PackageName: out[0].PackageName,
|
||||
Used: out[0].UsedFlowMb,
|
||||
Remaining: out[0].PackageTotalFlowMb - out[0].UsedFlowMb,
|
||||
Type: model.SimCardNameMap[out[0].CardType],
|
||||
PacketArr: nil,
|
||||
Remark: out[0].Remark,
|
||||
Grouping: "",
|
||||
DeliverDate: "",
|
||||
ActivateDate: oldApiDate(out[0].BossActivateAt, out[0].BossOpenAt, out[0].FlowPkgBeginAt),
|
||||
ExpireTime: oldApiEndOfDay(out[0].FlowPkgEndAt),
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageOldInput) (output *model.SimDataUsageOldOutput, err error) {
|
||||
input.Param.Iccid = strings.TrimSpace(input.Param.Iccid)
|
||||
if input.Param.Iccid == "" {
|
||||
return nil, gerror.New("iccid不能为空")
|
||||
}
|
||||
|
||||
card, err := oldApiScopedCardByIccid(ctx, input.Param.Iccid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SimDataUsageOldOutput{
|
||||
UseUnit: "MB",
|
||||
UseAmount: card.UsedFlowMb,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyOldInput) (output *model.SimDataUsageMonthlyOldOutput, err error) {
|
||||
input.Param.Iccid = strings.TrimSpace(input.Param.Iccid)
|
||||
input.Param.DateTime = strings.TrimSpace(input.Param.DateTime)
|
||||
if input.Param.Iccid == "" {
|
||||
return nil, gerror.New("iccid不能为空")
|
||||
}
|
||||
if input.Param.DateTime == "" {
|
||||
return nil, gerror.New("日期时间不能为空")
|
||||
}
|
||||
if _, err = oldApiScopedCardByIccid(ctx, input.Param.Iccid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
simDataUsageMonthlyInput := &model.SimDataUsageMonthlyInput{
|
||||
SimKeys: []*model.SimKey{
|
||||
{
|
||||
ICCID: input.Param.Iccid,
|
||||
},
|
||||
},
|
||||
ReqData: model.SimDataUsageMonthlyInputItem{
|
||||
QueryDate: input.Param.DateTime,
|
||||
},
|
||||
}
|
||||
out, err := service.UpstreamChan().QuerySimDatausageMonthly(ctx, simDataUsageMonthlyInput)
|
||||
if err != nil {
|
||||
return nil, gerror.New("查询sim卡历史用量失败")
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return &model.SimDataUsageMonthlyOldOutput{
|
||||
UseUnit: "MB",
|
||||
UseAmount: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
output = &model.SimDataUsageMonthlyOldOutput{
|
||||
UseUnit: "MB",
|
||||
UseAmount: out[0].ResData.DataAmount,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QuerySimStatus(ctx context.Context, input *model.SimStatusOldInput) (output *model.SimStatusOldOutput, err error) {
|
||||
input.Param.Iccid = strings.TrimSpace(input.Param.Iccid)
|
||||
if input.Param.Iccid == "" {
|
||||
return nil, gerror.New("iccid不能为空")
|
||||
}
|
||||
|
||||
card, err := oldApiScopedCardByIccid(ctx, input.Param.Iccid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := s.StatusConversion(*card)
|
||||
output = &model.SimStatusOldOutput{
|
||||
StatusMessage: model.SimCardStatusMap[status],
|
||||
StatusCode: status,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QuerySimSession(ctx context.Context, input *model.SimStatusOnlineOldInput) (output *model.SimStatusOnlineOldOutput, err error) {
|
||||
input.Param.Iccid = strings.TrimSpace(input.Param.Iccid)
|
||||
if input.Param.Iccid == "" {
|
||||
return nil, gerror.New("iccid不能为空")
|
||||
}
|
||||
if _, err = oldApiScopedCardByIccid(ctx, input.Param.Iccid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
simSessionInput := &model.SimSessionInput{
|
||||
SimKeys: []*model.SimKey{
|
||||
{
|
||||
ICCID: input.Param.Iccid,
|
||||
},
|
||||
},
|
||||
}
|
||||
outSession, err := service.UpstreamChan().QuerySimSession(ctx, simSessionInput)
|
||||
if err != nil {
|
||||
return nil, gerror.New("查询sim卡会话失败")
|
||||
}
|
||||
if len(outSession) == 0 {
|
||||
return &model.SimStatusOnlineOldOutput{
|
||||
TypeName: "",
|
||||
Imei: "",
|
||||
Data: model.SimStatusOnlineOldData{},
|
||||
StatusMessage: "",
|
||||
StatusCode: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
simStatusOnlineOldData := model.SimStatusOnlineOldData{
|
||||
ApnId: outSession[0].ResData.ApnId,
|
||||
Rat: outSession[0].ResData.Rat,
|
||||
Ipv6: outSession[0].ResData.Ipv6,
|
||||
Ip: outSession[0].ResData.Ipv4,
|
||||
Ipv6Prefix: outSession[0].ResData.Ipv6Prefix,
|
||||
Status: outSession[0].ResData.Status,
|
||||
CreateDate: outSession[0].ResData.CreateTime,
|
||||
}
|
||||
output = &model.SimStatusOnlineOldOutput{
|
||||
TypeName: "",
|
||||
Imei: "",
|
||||
Data: simStatusOnlineOldData,
|
||||
StatusMessage: "",
|
||||
StatusCode: 0,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sOldApiQuery) QueryCardInfoList(ctx context.Context, input *model.QueryCardInfoListInput) (output []model.QueryCardInfoListOutput, err error) {
|
||||
iccids := make([]string, 0, len(input.Param.Iccid))
|
||||
for _, iccid := range input.Param.Iccid {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if iccid == "" {
|
||||
continue
|
||||
}
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
if len(iccids) == 0 {
|
||||
return nil, gerror.New("iccid不能为空")
|
||||
}
|
||||
if len(iccids) > 100 {
|
||||
return nil, gerror.New("iccid数量不能超过100个")
|
||||
}
|
||||
|
||||
entity := []entity.TSimCardInfo{}
|
||||
if err := service.IotTSimCardInfo().
|
||||
Model(ctx).
|
||||
WhereIn(dao.TSimCardInfo.Columns().Iccid, iccids).
|
||||
Scan(&entity); err != nil {
|
||||
return nil, gerror.New("查询sim卡信息失败")
|
||||
}
|
||||
if len(entity) == 0 {
|
||||
return nil, gerror.New("sim卡不存在")
|
||||
}
|
||||
|
||||
output = make([]model.QueryCardInfoListOutput, 0, len(entity))
|
||||
for _, item := range entity {
|
||||
output = append(output, model.QueryCardInfoListOutput{
|
||||
Iccid: item.Iccid,
|
||||
ActivateDate: oldApiDate(item.BossActivateAt, item.BossOpenAt, item.FlowPkgBeginAt),
|
||||
DueExpireTime: oldApiEndOfDayString(item.FlowPkgEndAt),
|
||||
Imei: item.Imei,
|
||||
Imsi: item.Imsi,
|
||||
Msisdn: item.Msisdn,
|
||||
Type: model.SimCardNameMap[item.CardType],
|
||||
Remarks: item.Remark,
|
||||
Status: s.StatusConversion(item),
|
||||
})
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package openapiiot
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
type sSimCardInfo struct{}
|
||||
|
||||
func init() {
|
||||
service.RegisterIotTSimCardInfo(&sSimCardInfo{})
|
||||
}
|
||||
|
||||
func (s *sSimCardInfo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterAuth: true,
|
||||
FilterTenant: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TSimCardInfo.Ctx(ctx), option...)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
// 触发 service.RegisterIotTSimCardInfo,否则 loadSimBatchCards 会 panic
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/openapiiot"
|
||||
)
|
||||
|
||||
// setupImeiBatchDB 建一张最小可用的卡表并灌入覆盖各分支的样本数据。
|
||||
//
|
||||
// 样本设计(对齐 §7 测试场景):
|
||||
// CARD-A 有 imei → 正常命中
|
||||
// CARD-B imei 为空 → 卡存在但上游未上报值
|
||||
// CARD-C 有 imei → 正常命中(入库带的值)
|
||||
// CARD-D 属于另一个部门 → 数据权限过滤后视为 notFound
|
||||
func setupImeiBatchDB(t *testing.T, ctx context.Context) {
|
||||
t.Helper()
|
||||
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)
|
||||
}
|
||||
statements := []string{
|
||||
`CREATE TABLE t_sim_card_info (
|
||||
id INTEGER PRIMARY KEY,
|
||||
iccid TEXT, msisdn TEXT, imsi TEXT, imei TEXT,
|
||||
boss_card_status INTEGER, used_flow_mb REAL,
|
||||
imei_sync_time DATETIME, imei_update_time DATETIME,
|
||||
dept_id INTEGER, tenant_id TEXT
|
||||
)`,
|
||||
// FilterAuth 的 GetDeptAndSub 会查 sys_dept(本部门及以下数据权限)
|
||||
`CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, ancestors TEXT)`,
|
||||
`INSERT INTO t_sim_card_info
|
||||
(id, iccid, msisdn, imsi, imei, boss_card_status, used_flow_mb, imei_sync_time, imei_update_time, dept_id, tenant_id)
|
||||
VALUES
|
||||
(1,'CARD-A','13800000001','460000000000001','865466070817587',2,10.5,'2026-10-12 04:31:07','2026-10-12 04:31:07',7,'A'),
|
||||
(2,'CARD-B','13800000002','460000000000002','', 2,20.0,'2026-10-12 04:31:09',NULL, 7,'A'),
|
||||
(3,'CARD-C','13800000003','460000000000003','123456789012345',2,30.0,NULL, '2026-09-01 08:00:00',7,'A'),
|
||||
(4,'CARD-D','13800000004','460000000000004','999888777666555',2,40.0,'2026-10-12 04:31:11','2026-10-12 04:31:11',8,'A')`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apiDataScopeRoleId 对齐 middleware.go:28 的 apiDataScopeRoleId(包私有,此处复刻值)。
|
||||
const apiDataScopeRoleId int64 = 17
|
||||
|
||||
// imeiBatchCtx 复现 ApiCtx 中间件注入的身份:API 客户固定 RoleId=17 +
|
||||
// SysRoleDataScopeDeptAndBelow(对齐 middleware.go:99),使数据权限过滤真实生效。
|
||||
func imeiBatchCtx() context.Context {
|
||||
ctx := contexts.Set(context.Background(), &model.Context{
|
||||
User: &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: 0,
|
||||
TenantId: "A",
|
||||
DeptId: 7,
|
||||
Roles: []model.Role{
|
||||
{RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow},
|
||||
},
|
||||
},
|
||||
},
|
||||
AgentUser: &model.BaseClaimsApi{DeptId: 7, TenantId: "A"},
|
||||
})
|
||||
contexts.SetDataValue(ctx, "currentRoleId", gconv.String(apiDataScopeRoleId))
|
||||
contexts.SetDataValue(ctx, "currentRoleDataScope", consts.SysRoleDataScopeDeptAndBelow)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// TestQuerySimImeiBatchSemantics 覆盖 §7 T11/T12/T16 的核心语义:
|
||||
// - 有值的卡返回 imei
|
||||
// - imei 为空但卡存在 → matched=true 且 imei="",不算 notFound(T16)
|
||||
// - 不存在的卡 → matched=false + CARD_NOT_FOUND(T12)
|
||||
// - 跨部门的卡 → 数据权限过滤后视为 notFound(T13)
|
||||
func TestQuerySimImeiBatchSemantics(t *testing.T) {
|
||||
ctx := imeiBatchCtx()
|
||||
setupImeiBatchDB(t, ctx)
|
||||
|
||||
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{
|
||||
{Iccid: "CARD-A"},
|
||||
{Iccid: "CARD-B"},
|
||||
{Iccid: "CARD-C"},
|
||||
{Iccid: "CARD-D"},
|
||||
{Iccid: "CARD-MISSING"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if output.Total != 5 {
|
||||
t.Fatalf("total = %d, want 5", output.Total)
|
||||
}
|
||||
// A/B/C 命中,D(跨部门)与 MISSING 未命中
|
||||
if output.MatchedCount != 3 || output.NotFoundCount != 2 {
|
||||
t.Fatalf("matched=%d notFound=%d, want 3/2", output.MatchedCount, output.NotFoundCount)
|
||||
}
|
||||
if len(output.Items) != 5 {
|
||||
t.Fatalf("items = %d, want 5 (按输入顺序逐卡返回)", len(output.Items))
|
||||
}
|
||||
|
||||
// 顺序必须与输入一致(契约:按输入顺序逐卡返回)。
|
||||
// 注意:未命中的 item 不返回 iccid(omitempty),所以必须用下标访问,
|
||||
// 不能用 iccid 建索引,否则未命中项会取到零值。
|
||||
ordered := []string{"CARD-A", "CARD-B", "CARD-C", "CARD-D", "CARD-MISSING"}
|
||||
for i, wantIccid := range ordered {
|
||||
if i == 3 || i == 4 {
|
||||
continue // D 与 MISSING 未命中,不返回 iccid
|
||||
}
|
||||
if output.Items[i].Iccid != wantIccid {
|
||||
t.Fatalf("items[%d].iccid = %q, want %q(必须保持输入顺序)", i, output.Items[i].Iccid, wantIccid)
|
||||
}
|
||||
}
|
||||
a, b, c := output.Items[0], output.Items[1], output.Items[2]
|
||||
d, missing := output.Items[3], output.Items[4]
|
||||
if !a.Matched || a.Imei != "865466070817587" {
|
||||
t.Fatalf("CARD-A: %+v", a)
|
||||
}
|
||||
|
||||
// CARD-B:imei 为空但 matched=true(关键语义,不算 notFound)
|
||||
if !b.Matched {
|
||||
t.Fatalf("CARD-B 应为 matched=true(卡存在,只是上游没给值): %+v", b)
|
||||
}
|
||||
if b.Imei != "" {
|
||||
t.Fatalf("CARD-B imei 应为空串: %q", b.Imei)
|
||||
}
|
||||
|
||||
// CARD-C:入库就带值
|
||||
if !c.Matched || c.Imei != "123456789012345" {
|
||||
t.Fatalf("CARD-C: %+v", c)
|
||||
}
|
||||
|
||||
// CARD-D / MISSING:未命中,且不泄露 imei
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
item model.QuerySimImeiBatchItem
|
||||
}{{"CARD-D(跳部门)", d}, {"CARD-MISSING(不存在)", missing}} {
|
||||
item := tc.item
|
||||
if item.Matched {
|
||||
t.Fatalf("%s 不应命中: %+v", tc.name, item)
|
||||
}
|
||||
if item.ErrorCode != "CARD_NOT_FOUND" {
|
||||
t.Fatalf("%s errorCode = %q, want CARD_NOT_FOUND", tc.name, item.ErrorCode)
|
||||
}
|
||||
if item.Imei != "" {
|
||||
t.Fatalf("%s 未命中时不应返回 imei: %q", tc.name, item.Imei)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuerySimImeiBatchValidation 覆盖 §7 T14/T15 的参数校验。
|
||||
func TestQuerySimImeiBatchValidation(t *testing.T) {
|
||||
ctx := imeiBatchCtx()
|
||||
setupImeiBatchDB(t, ctx)
|
||||
svc := NewQuerySim()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in *model.QuerySimImeiBatchParam
|
||||
}{
|
||||
{"nil 参数", nil},
|
||||
{"appId 为空", &model.QuerySimImeiBatchParam{Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}}}},
|
||||
{"cards 为空", &model.QuerySimImeiBatchParam{AppId: "app"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if _, err := svc.QuerySimImeiBatch(ctx, tc.in); err == nil {
|
||||
t.Fatalf("%s: 应返回错误", tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 超过 1000 张(T14)
|
||||
tooMany := make([]model.QuerySimBatchCard, 0, querySimBatchMaxCards+1)
|
||||
for i := 0; i <= querySimBatchMaxCards; i++ {
|
||||
tooMany = append(tooMany, model.QuerySimBatchCard{Iccid: "CARD-" + string(rune('0'+i%10)) + string(rune('0'+i/10%10)) + string(rune('0'+i/100%10)) + string(rune('0'+i/1000%10))})
|
||||
}
|
||||
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{AppId: "app", Cards: tooMany}); err == nil {
|
||||
t.Fatal("超过 1000 张应被拒绝")
|
||||
}
|
||||
|
||||
// 重复标识(parseSimBatchIdentifiers 既有逻辑)
|
||||
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-A"}},
|
||||
}); err == nil {
|
||||
t.Fatal("重复标识应被拒绝")
|
||||
}
|
||||
|
||||
// 单标识超长(simBatchIdentifierMaxLen)
|
||||
longIccid := make([]byte, simBatchIdentifierMaxLen+1)
|
||||
for i := range longIccid {
|
||||
longIccid[i] = '9'
|
||||
}
|
||||
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{{Iccid: string(longIccid)}},
|
||||
}); err == nil {
|
||||
t.Fatal("超长标识应被拒绝")
|
||||
}
|
||||
|
||||
// 多标识互相矛盾:ICCID 属于 CARD-A 但 MSISDN 属于 CARD-B(既有校验逻辑)
|
||||
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("多标识查询本身应合法: %v", err)
|
||||
}
|
||||
// 矛盾组合应返回 notFound 而非错误(simBatchCardMatchesIdentifier 拦下)
|
||||
conflict, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if conflict.NotFoundCount != 1 || conflict.Items[0].Matched {
|
||||
t.Fatalf("矛盾的 iccid+msisdn 组合应判为 notFound: %+v", conflict.Items[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuerySimImeiBatchByMsisdnAndImsi 验证三种标识都能定位到卡。
|
||||
func TestQuerySimImeiBatchByMsisdnAndImsi(t *testing.T) {
|
||||
ctx := imeiBatchCtx()
|
||||
setupImeiBatchDB(t, ctx)
|
||||
|
||||
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{
|
||||
{Msisdn: "13800000001"},
|
||||
{Imsi: "460000000000003"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if output.MatchedCount != 2 {
|
||||
t.Fatalf("matched = %d, want 2", output.MatchedCount)
|
||||
}
|
||||
for _, item := range output.Items {
|
||||
if !item.Matched || item.Iccid == "" {
|
||||
t.Fatalf("应通过 msisdn/imsi 定位到卡并回填 iccid: %+v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuerySimImeiBatchItemJSONShape 锁死对外字段集:只允许
|
||||
// matched / iccid / imei / errorCode / errorMessage,任何额外字段(例如
|
||||
// imeiSyncTime、imeiUpdateTime)都会让该用例失败,避免内部字段泄露给客户。
|
||||
func TestQuerySimImeiBatchItemJSONShape(t *testing.T) {
|
||||
ctx := imeiBatchCtx()
|
||||
setupImeiBatchDB(t, ctx)
|
||||
|
||||
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
|
||||
AppId: "app",
|
||||
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-MISSING"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
allowed := map[string]bool{
|
||||
"matched": true, "iccid": true, "imei": true,
|
||||
"errorCode": true, "errorMessage": true,
|
||||
}
|
||||
for i, item := range output.Items {
|
||||
raw, marshalErr := json.Marshal(item)
|
||||
if marshalErr != nil {
|
||||
t.Fatalf("items[%d] 序列化失败: %v", i, marshalErr)
|
||||
}
|
||||
var fields map[string]any
|
||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||
t.Fatalf("items[%d] 反序列化失败: %v", i, err)
|
||||
}
|
||||
for name := range fields {
|
||||
if !allowed[name] {
|
||||
t.Fatalf("items[%d] 出现非预期对外字段 %q: %s", i, name, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
)
|
||||
|
||||
// querySimBatchMaxCards 批量查询单次的卡数量上限(与 QueryTrafficCutoffConfig 同量级)。
|
||||
// 纯本地库读、不调上游,故偏保守;如需更大块,改这一个常量即可(注意 IN 列表与报文大小)。
|
||||
const querySimBatchMaxCards = 1000
|
||||
|
||||
// simBatchIdentifierMaxLen 单标识允许的最大字符数,对齐 t_sim_card_info 列宽(varchar(32))并留余量,
|
||||
// 用于把请求体内存占用钉死在可控范围(1000 张 * 3 字段 * 64 ≈ 192KB),防止超长字符串滥用。
|
||||
const simBatchIdentifierMaxLen = 64
|
||||
|
||||
type simBatchIdentifier struct {
|
||||
Msisdn string
|
||||
Iccid string
|
||||
Imsi string
|
||||
}
|
||||
|
||||
func validateSimBatchIdentifier(item simBatchIdentifier) error {
|
||||
count := 0
|
||||
if item.Msisdn != "" {
|
||||
if len(item.Msisdn) > simBatchIdentifierMaxLen {
|
||||
return gerror.New("msisdn长度超限")
|
||||
}
|
||||
count++
|
||||
}
|
||||
if item.Iccid != "" {
|
||||
if len(item.Iccid) > simBatchIdentifierMaxLen {
|
||||
return gerror.New("iccid长度超限")
|
||||
}
|
||||
count++
|
||||
}
|
||||
if item.Imsi != "" {
|
||||
if len(item.Imsi) > simBatchIdentifierMaxLen {
|
||||
return gerror.New("imsi长度超限")
|
||||
}
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("msisdn、iccid、imsi至少传一个")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// simBatchCardMatchesIdentifier 校验返回的卡是否同时匹配输入里提供的所有标识。
|
||||
// 防止单条输入同时填了多个互相矛盾的标识(如 iccid 属于 A、msisdn 属于 B)时,
|
||||
// 仍返回优先级最高那个标识对应卡的数据。沿用 traffic_cutoff_config.go 的同名校验逻辑。
|
||||
func simBatchCardMatchesIdentifier(card *entity.TSimCardInfo, identifier simBatchIdentifier) 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 simBatchIdentifierKey(item simBatchIdentifier) string {
|
||||
if item.Iccid != "" {
|
||||
return "iccid:" + item.Iccid
|
||||
}
|
||||
if item.Msisdn != "" {
|
||||
return "msisdn:" + item.Msisdn
|
||||
}
|
||||
return "imsi:" + item.Imsi
|
||||
}
|
||||
|
||||
// parseSimBatchIdentifiers 校验并去重,返回按 iccid/msisdn/imsi 分桶的标识列表。
|
||||
func parseSimBatchIdentifiers(cards []model.QuerySimBatchCard) ([]simBatchIdentifier, error) {
|
||||
identifiers := make([]simBatchIdentifier, 0, len(cards))
|
||||
seen := make(map[string]struct{}, len(cards))
|
||||
for _, item := range cards {
|
||||
identifier := simBatchIdentifier{
|
||||
Msisdn: strings.TrimSpace(item.Msisdn),
|
||||
Iccid: strings.TrimSpace(item.Iccid),
|
||||
Imsi: strings.TrimSpace(item.Imsi),
|
||||
}
|
||||
if err := validateSimBatchIdentifier(identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := simBatchIdentifierKey(identifier)
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, gerror.Newf("卡片标识重复:%s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
identifiers = append(identifiers, identifier)
|
||||
}
|
||||
return identifiers, nil
|
||||
}
|
||||
|
||||
// loadSimBatchCards 按标识分桶做 WhereIn 查询(自动带部门数据权限),返回 type:key -> 卡 的映射。
|
||||
func loadSimBatchCards(ctx context.Context, identifiers []simBatchIdentifier) (map[string]*entity.TSimCardInfo, error) {
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
result := make(map[string]*entity.TSimCardInfo, len(identifiers))
|
||||
lookup := func(field string, values []string) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
var cards []*entity.TSimCardInfo
|
||||
err := service.IotTSimCardInfo().Model(ctx).
|
||||
Fields(columns.Iccid, columns.Msisdn, columns.Imsi, columns.BossCardStatus, columns.UsedFlowMb,
|
||||
columns.Imei).
|
||||
WhereIn(field, values).Scan(&cards)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "批量查询卡信息失败")
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card == nil {
|
||||
continue
|
||||
}
|
||||
if card.Iccid != "" {
|
||||
result["iccid:"+strings.TrimSpace(card.Iccid)] = card
|
||||
}
|
||||
if card.Msisdn != "" {
|
||||
result["msisdn:"+strings.TrimSpace(card.Msisdn)] = card
|
||||
}
|
||||
if card.Imsi != "" {
|
||||
result["imsi:"+strings.TrimSpace(card.Imsi)] = card
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
iccidValues, msisdnValues, imsiValues := make([]string, 0), make([]string, 0), make([]string, 0)
|
||||
for _, item := range identifiers {
|
||||
switch {
|
||||
case item.Iccid != "":
|
||||
iccidValues = append(iccidValues, item.Iccid)
|
||||
case item.Msisdn != "":
|
||||
msisdnValues = append(msisdnValues, item.Msisdn)
|
||||
default:
|
||||
imsiValues = append(imsiValues, item.Imsi)
|
||||
}
|
||||
}
|
||||
if err := lookup(columns.Iccid, iccidValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lookup(columns.Msisdn, msisdnValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lookup(columns.Imsi, imsiValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// QuerySimStatusBatch 批量查询卡状态(纯读本地库,不调上游通道)。
|
||||
// 返回结构与单卡 QuerySimStatus 一致(iccid / lifeCycle 内部码 / lifeCycleName),
|
||||
// 但按输入顺序逐卡返回,未匹配项用 Matched=false + errorCode 标记。
|
||||
func (s *sQuerySim) QuerySimStatusBatch(ctx context.Context, in *model.QuerySimStatusBatchParam) (*model.QuerySimStatusBatchOutput, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("查询参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return nil, gerror.New("查询卡不能为空")
|
||||
}
|
||||
if len(in.Cards) > querySimBatchMaxCards {
|
||||
return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards)
|
||||
}
|
||||
identifiers, err := parseSimBatchIdentifiers(in.Cards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardMap, err := loadSimBatchCards(ctx, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := &model.QuerySimStatusBatchOutput{
|
||||
Total: len(identifiers),
|
||||
Items: make([]model.QuerySimStatusBatchItem, 0, len(identifiers)),
|
||||
}
|
||||
for _, identifier := range identifiers {
|
||||
result := model.QuerySimStatusBatchItem{}
|
||||
card := cardMap[simBatchIdentifierKey(identifier)]
|
||||
if card == nil || !simBatchCardMatchesIdentifier(card, identifier) {
|
||||
result.ErrorCode = "CARD_NOT_FOUND"
|
||||
result.ErrorMessage = "卡片不存在或无权限"
|
||||
output.NotFoundCount++
|
||||
output.Items = append(output.Items, result)
|
||||
continue
|
||||
}
|
||||
result.Matched = true
|
||||
result.Iccid = strings.TrimSpace(card.Iccid)
|
||||
statusName, ok := consts.V3ApiSimBossCardStatusNameMap[card.BossCardStatus]
|
||||
if !ok {
|
||||
statusName = "未知"
|
||||
}
|
||||
result.LifeCycle = card.BossCardStatus
|
||||
result.LifeCycleName = statusName
|
||||
output.MatchedCount++
|
||||
output.Items = append(output.Items, result)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// QuerySimUsageBatch 批量查询卡流量(used_flow_mb,纯读本地库)。
|
||||
func (s *sQuerySim) QuerySimUsageBatch(ctx context.Context, in *model.QuerySimUsageBatchParam) (*model.QuerySimUsageBatchOutput, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("查询参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return nil, gerror.New("查询卡不能为空")
|
||||
}
|
||||
if len(in.Cards) > querySimBatchMaxCards {
|
||||
return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards)
|
||||
}
|
||||
identifiers, err := parseSimBatchIdentifiers(in.Cards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardMap, err := loadSimBatchCards(ctx, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := &model.QuerySimUsageBatchOutput{
|
||||
Total: len(identifiers),
|
||||
Items: make([]model.QuerySimUsageBatchItem, 0, len(identifiers)),
|
||||
}
|
||||
for _, identifier := range identifiers {
|
||||
result := model.QuerySimUsageBatchItem{}
|
||||
card := cardMap[simBatchIdentifierKey(identifier)]
|
||||
if card == nil || !simBatchCardMatchesIdentifier(card, identifier) {
|
||||
result.ErrorCode = "CARD_NOT_FOUND"
|
||||
result.ErrorMessage = "卡片不存在或无权限"
|
||||
output.NotFoundCount++
|
||||
output.Items = append(output.Items, result)
|
||||
continue
|
||||
}
|
||||
result.Matched = true
|
||||
result.Iccid = strings.TrimSpace(card.Iccid)
|
||||
result.UseUnit = "MB"
|
||||
result.UseAmount = card.UsedFlowMb
|
||||
output.MatchedCount++
|
||||
output.Items = append(output.Items, result)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// QuerySimImeiBatch 批量查询卡 IMEI(纯读本地库,不调上游通道)。
|
||||
//
|
||||
// 数据来源是 t_sim_card_info.imei 快照,由 worker 的 IMEI 全量同步链路维护;
|
||||
// 另有一个补充来源:单卡 /openapi/v3/query/sim-imei 在库值为空时会实时调运营商
|
||||
// 并回写(不写 imei_sync_time,只写 imei_update_time)。
|
||||
//
|
||||
// 语义要点:
|
||||
// - imei 为空**不算 notFound**:卡存在但上游从未查到值(如不支持的模板通道)。
|
||||
// - 本接口**不触发任何上游请求**,是纯本地库读(与 sim-status-batch 一致)。
|
||||
func (s *sQuerySim) QuerySimImeiBatch(ctx context.Context, in *model.QuerySimImeiBatchParam) (*model.QuerySimImeiBatchOutput, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("查询参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return nil, gerror.New("查询卡不能为空")
|
||||
}
|
||||
if len(in.Cards) > querySimBatchMaxCards {
|
||||
return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards)
|
||||
}
|
||||
identifiers, err := parseSimBatchIdentifiers(in.Cards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardMap, err := loadSimBatchCards(ctx, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := &model.QuerySimImeiBatchOutput{
|
||||
Total: len(identifiers),
|
||||
Items: make([]model.QuerySimImeiBatchItem, 0, len(identifiers)),
|
||||
}
|
||||
for _, identifier := range identifiers {
|
||||
result := model.QuerySimImeiBatchItem{}
|
||||
card := cardMap[simBatchIdentifierKey(identifier)]
|
||||
if card == nil || !simBatchCardMatchesIdentifier(card, identifier) {
|
||||
result.ErrorCode = "CARD_NOT_FOUND"
|
||||
result.ErrorMessage = "卡片不存在或无权限"
|
||||
output.NotFoundCount++
|
||||
output.Items = append(output.Items, result)
|
||||
continue
|
||||
}
|
||||
result.Matched = true
|
||||
result.Iccid = strings.TrimSpace(card.Iccid)
|
||||
result.Imei = strings.TrimSpace(card.Imei)
|
||||
output.MatchedCount++
|
||||
output.Items = append(output.Items, result)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type batchStatusRecord struct {
|
||||
Id int64 `orm:"id"`
|
||||
BatchNo string `orm:"batch_no"`
|
||||
BatchAsyncStatus int `orm:"batch_async_status"`
|
||||
SourceChannel string `orm:"source_channel"`
|
||||
ExecutionMode int `orm:"execution_mode"`
|
||||
SourceType int `orm:"source_type"`
|
||||
TotalCount int `orm:"total_count"`
|
||||
ValidCount int `orm:"valid_count"`
|
||||
SuccessCount int `orm:"success_count"`
|
||||
FailCount int `orm:"fail_count"`
|
||||
RunResult int `orm:"run_result"`
|
||||
FailReason string `orm:"fail_reason"`
|
||||
}
|
||||
|
||||
func (s *sQuerySim) QueryBatchStatus(ctx context.Context, in *model.QueryBatchStatusParam) (*model.QueryBatchStatusOutput, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("查询参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("batchNo不能为空")
|
||||
}
|
||||
tenantID := strings.TrimSpace(contexts.GetTenantId(ctx))
|
||||
deptID := contexts.GetAgentDeptId(ctx)
|
||||
if tenantID == "" || deptID <= 0 {
|
||||
return nil, gerror.New("当前接口调用方信息不完整")
|
||||
}
|
||||
columns := dao.BatchMain.Columns()
|
||||
var batch batchStatusRecord
|
||||
err := dao.BatchMain.Ctx(ctx).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(fmt.Sprintf("NOT (%s = ? AND COALESCE(%s, '') <> '' AND %s <> %s)", columns.BatchType, columns.SourceBatchNo, columns.SourceBatchNo, columns.BatchNo), consts.BatchTypeAllocate).
|
||||
Scan(&batch)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询批次状态失败")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在或无权限")
|
||||
}
|
||||
return &model.QueryBatchStatusOutput{
|
||||
BatchNo: batch.BatchNo,
|
||||
BatchAsyncStatus: batch.BatchAsyncStatus,
|
||||
SourceChannel: batch.SourceChannel,
|
||||
ExecutionMode: batch.ExecutionMode,
|
||||
SourceType: batch.SourceType,
|
||||
TotalCount: batch.TotalCount,
|
||||
ValidCount: batch.ValidCount,
|
||||
SuccessCount: batch.SuccessCount,
|
||||
FailCount: batch.FailCount,
|
||||
RunResult: batch.RunResult,
|
||||
FailReason: batch.FailReason,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package query
|
||||
|
||||
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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
)
|
||||
|
||||
func TestBatchStatusScopeAndAllocationChildren(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)
|
||||
}
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE batch_main (id INTEGER PRIMARY KEY, batch_no TEXT, batch_type INTEGER, source_batch_no TEXT, tenant_id TEXT, dept_id INTEGER, batch_async_status INTEGER, source_channel TEXT, execution_mode INTEGER, source_type INTEGER, total_count INTEGER, valid_count INTEGER, success_count INTEGER, fail_count INTEGER, run_result INTEGER, fail_reason TEXT)",
|
||||
"INSERT INTO batch_main VALUES (1,'mine',101,'mine','A',7,3,'api',1,2,1,1,1,0,2,''),(2,'other-tenant',101,'','B',7,3,'api',1,2,1,1,1,0,2,''),(3,'other-dept',101,'','A',8,3,'api',1,2,1,1,1,0,2,''),(4,'parent',3,'','A',7,2,'api',2,2,2,2,1,0,1,''),(5,'child',3,'parent','A',7,2,'api',2,2,1,1,0,0,1,''),(6,'self',3,'self','A',7,3,'api',2,2,1,1,1,0,2,'')",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
ctx = contexts.Set(ctx, &model.Context{
|
||||
User: &model.Identity{BaseClaims: model.BaseClaims{TenantId: "A"}},
|
||||
AgentUser: &model.BaseClaimsApi{DeptId: 7, TenantId: "A"},
|
||||
})
|
||||
for _, batchNo := range []string{"mine", "parent", "self"} {
|
||||
result, err := NewQuerySim().QueryBatchStatus(ctx, &model.QueryBatchStatusParam{AppId: "app", BatchNo: batchNo})
|
||||
if err != nil || result == nil || result.BatchNo != batchNo || result.SourceChannel != "api" {
|
||||
t.Fatalf("batch %s: result=%+v err=%v", batchNo, result, err)
|
||||
}
|
||||
}
|
||||
for _, batchNo := range []string{"other-tenant", "other-dept", "child", "missing"} {
|
||||
if _, err := NewQuerySim().QueryBatchStatus(ctx, &model.QueryBatchStatusParam{AppId: "app", BatchNo: batchNo}); err == nil {
|
||||
t.Fatalf("batch exposed: %s", batchNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sQuerySim struct {
|
||||
}
|
||||
|
||||
func NewQuerySim() *sQuerySim {
|
||||
return &sQuerySim{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterQuerySim(NewQuerySim())
|
||||
}
|
||||
|
||||
func (s *sQuerySim) querySim(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimInfoCard, err error) {
|
||||
if input.Iccid == "" && input.Msisdn == "" && input.Imsi == "" {
|
||||
g.Log().Debugf(ctx, "QuerySim.QuerySimCard, Iccid, Msisdn, Imsi are all empty, input: %v", input)
|
||||
return nil, gerror.New("Iccid, Msisdn, Imsi 不能全为空")
|
||||
}
|
||||
field := ""
|
||||
value := ""
|
||||
if input.Iccid != "" {
|
||||
field = dao.TSimCardInfo.Columns().Iccid
|
||||
value = input.Iccid
|
||||
} else if input.Msisdn != "" {
|
||||
field = dao.TSimCardInfo.Columns().Msisdn
|
||||
value = input.Msisdn
|
||||
} else if input.Imsi != "" {
|
||||
field = dao.TSimCardInfo.Columns().Imsi
|
||||
value = input.Imsi
|
||||
}
|
||||
|
||||
cardInfoTable := dao.TSimCardInfo.Table()
|
||||
sysDept := dao.SysDept.Table()
|
||||
flowPackage := dao.TSimFlowPackageInfo.Table()
|
||||
packageCategory := dao.TSimPackageCategory.Table()
|
||||
|
||||
err = service.IotTSimCardInfo().
|
||||
Model(ctx).
|
||||
Where(field, value).
|
||||
LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().DeptId).
|
||||
LeftJoin(flowPackage, flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode).
|
||||
LeftJoin(packageCategory, packageCategory+"."+dao.TSimPackageCategory.Columns().CategoryCode+"="+flowPackage+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode).
|
||||
Fields(
|
||||
cardInfoTable+".*",
|
||||
sysDept+"."+dao.SysDept.Columns().DeptName+" as DeptName",
|
||||
sysDept+"."+dao.SysDept.Columns().DeptKey+" as DeptKey",
|
||||
sysDept+"."+dao.SysDept.Columns().AreaCode+" as DeptAreaCode",
|
||||
flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageName+" as PackageName",
|
||||
flowPackage+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" as CategoryCode",
|
||||
packageCategory+"."+dao.TSimPackageCategory.Columns().CategoryName+" as CategoryName",
|
||||
).
|
||||
Limit(1).
|
||||
Scan(&output)
|
||||
if err != nil {
|
||||
g.Log().Debugf(ctx, "QuerySim.QuerySimCard, List sim card info failed, input: %v, err: %v", input, err)
|
||||
return nil, gerror.New("查询sim卡信息失败,请确认ICCID,MSISDN,IMSI是否正确")
|
||||
}
|
||||
if output == nil {
|
||||
return nil, gerror.New("查询sim卡信息失败,请确认ICCID,MSISDN,IMSI是否正确")
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func queryDate(t *gtime.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d")
|
||||
}
|
||||
|
||||
func queryDateTime(t *gtime.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
|
||||
func queryRemaining(total, used float64) string {
|
||||
remaining := total - used
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
return fmt.Sprintf("%.1f", remaining)
|
||||
}
|
||||
|
||||
func queryRemainingInt(total, used int) string {
|
||||
remaining := total - used
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
return fmt.Sprintf("%d", remaining)
|
||||
}
|
||||
|
||||
func queryValidityPeriod(begin, end *gtime.Time) int {
|
||||
if begin == nil || begin.IsZero() || end == nil || end.IsZero() {
|
||||
return 0
|
||||
}
|
||||
beginTime := begin.Time
|
||||
endTime := end.Time
|
||||
months := (endTime.Year()-beginTime.Year())*12 + int(endTime.Month()-beginTime.Month())
|
||||
if endTime.Day() >= beginTime.Day()-1 {
|
||||
months++
|
||||
}
|
||||
if months < 0 {
|
||||
return 0
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
func possibleRegionCodes(regionCode string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
codes := make([]string, 0, 4)
|
||||
add := func(code string) {
|
||||
if code == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[code]; ok {
|
||||
return
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
codes = append(codes, code)
|
||||
}
|
||||
|
||||
add(regionCode)
|
||||
if len(regionCode) == 12 {
|
||||
if regionCode[0:2] != "00" {
|
||||
add(regionCode[0:2] + "0000000000")
|
||||
}
|
||||
if regionCode[2:4] != "00" {
|
||||
add(regionCode[0:4] + "00000000")
|
||||
}
|
||||
if regionCode[4:6] != "00" {
|
||||
add(regionCode[0:6] + "000000")
|
||||
}
|
||||
} else {
|
||||
if len(regionCode) >= 4 {
|
||||
add(regionCode[:2] + "0000")
|
||||
}
|
||||
if len(regionCode) >= 6 {
|
||||
add(regionCode[:4] + "00")
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func (s *sQuerySim) queryLocation(ctx context.Context, regionCode string) string {
|
||||
regionCode = strings.TrimSpace(regionCode)
|
||||
if regionCode == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
type division struct {
|
||||
RegionCode string `json:"regionCode"`
|
||||
RegionName string `json:"regionName"`
|
||||
RegionParent string `json:"regionParent"`
|
||||
}
|
||||
|
||||
var divisions []*division
|
||||
err := dao.TDivisionCode.Ctx(ctx).
|
||||
WhereIn(dao.TDivisionCode.Columns().RegionCode, possibleRegionCodes(regionCode)).
|
||||
Scan(&divisions)
|
||||
if err != nil || len(divisions) == 0 {
|
||||
return regionCode
|
||||
}
|
||||
|
||||
byCode := make(map[string]*division, len(divisions))
|
||||
for _, item := range divisions {
|
||||
byCode[item.RegionCode] = item
|
||||
}
|
||||
item := byCode[regionCode]
|
||||
if item == nil {
|
||||
return regionCode
|
||||
}
|
||||
|
||||
names := make([]string, 0, 4)
|
||||
for item != nil {
|
||||
if item.RegionName != "" {
|
||||
names = append([]string{item.RegionName}, names...)
|
||||
}
|
||||
if item.RegionParent == "" || item.RegionParent == "0" {
|
||||
break
|
||||
}
|
||||
item = byCode[item.RegionParent]
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return regionCode
|
||||
}
|
||||
if names[0] != "中国" {
|
||||
names = append([]string{"中国"}, names...)
|
||||
}
|
||||
return strings.Join(names, "/")
|
||||
}
|
||||
|
||||
func (s *sQuerySim) queryLocationByAreaCode(ctx context.Context, areaCode string) string {
|
||||
areaCode = strings.TrimSpace(areaCode)
|
||||
if areaCode == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
type division struct {
|
||||
RegionCode string `json:"regionCode"`
|
||||
RegionName string `json:"regionName"`
|
||||
RegionParent string `json:"regionParent"`
|
||||
}
|
||||
|
||||
byCode := make(map[string]*division)
|
||||
currentCode := ""
|
||||
for _, code := range possibleRegionCodes(areaCode) {
|
||||
var item *division
|
||||
err := dao.TDivisionCode.Ctx(ctx).
|
||||
Where(dao.TDivisionCode.Columns().RegionCode, code).
|
||||
Scan(&item)
|
||||
if err != nil {
|
||||
return areaCode
|
||||
}
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
byCode[item.RegionCode] = item
|
||||
currentCode = item.RegionCode
|
||||
break
|
||||
}
|
||||
if currentCode == "" {
|
||||
return areaCode
|
||||
}
|
||||
|
||||
names := make([]string, 0, 4)
|
||||
for currentCode != "" {
|
||||
item := byCode[currentCode]
|
||||
if item == nil {
|
||||
err := dao.TDivisionCode.Ctx(ctx).
|
||||
Where(dao.TDivisionCode.Columns().RegionCode, currentCode).
|
||||
Scan(&item)
|
||||
if err != nil {
|
||||
return areaCode
|
||||
}
|
||||
if item == nil {
|
||||
break
|
||||
}
|
||||
byCode[item.RegionCode] = item
|
||||
}
|
||||
if item.RegionName != "" {
|
||||
names = append([]string{item.RegionName}, names...)
|
||||
}
|
||||
if item.RegionParent == "" || item.RegionParent == "0" {
|
||||
break
|
||||
}
|
||||
currentCode = item.RegionParent
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return areaCode
|
||||
}
|
||||
if names[0] != "中国" {
|
||||
names = append([]string{"中国"}, names...)
|
||||
}
|
||||
return strings.Join(names, "/")
|
||||
}
|
||||
|
||||
func (s *sQuerySim) QuerySimInfo(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimInfoOutput, err error) {
|
||||
g.Log().Error(ctx, input)
|
||||
simCard, err := s.querySim(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output = &model.QuerySimInfoOutput{
|
||||
Iccid: simCard.Iccid,
|
||||
Msisdn: simCard.Msisdn,
|
||||
Imsi: simCard.Imsi,
|
||||
Imei: simCard.Imei,
|
||||
Username: simCard.DeptName,
|
||||
Location: s.queryLocationByAreaCode(ctx, simCard.DeptAreaCode),
|
||||
PlanCategory: simCard.CategoryName,
|
||||
CurrentPlanName: simCard.PackageName,
|
||||
Plangroup: "",
|
||||
Planid: simCard.CurrentPackageCode,
|
||||
Appid: simCard.DeptKey,
|
||||
AllowPlanChange: 0,
|
||||
LastSyncTime: queryDateTime(simCard.StatusSyncTime),
|
||||
AccessType: consts.V3ApiSimAccessTypeNameMap[simCard.AccessType],
|
||||
CardStatus: consts.V3ApiSimBossCardStatusNameMap[simCard.BossCardStatus],
|
||||
CardType: consts.V3ApiSimCardTypeNameMap[simCard.CardType],
|
||||
NetworkType: consts.V3ApiSimNetModelNameMap[simCard.NetModel],
|
||||
ActivationDate: queryDate(simCard.OutStockTime),
|
||||
ActiveDate: queryDate(simCard.FlowPkgBeginAt),
|
||||
ValidityPeriod: queryValidityPeriod(simCard.FlowPkgBeginAt, simCard.FlowPkgEndAt),
|
||||
ExpiryDate: queryDate(simCard.FlowPkgEndAt),
|
||||
TestPeriod: 0,
|
||||
TestPeriodExpiryDate: nil,
|
||||
SilentPeriod: 0,
|
||||
SilentPeriodExpiryDate: nil,
|
||||
DataService: simCard.DataServiceStatus,
|
||||
PlanData: int(math.Round(simCard.PackageTotalFlowMb)),
|
||||
UsedData: simCard.UsedFlowMb,
|
||||
RemainingData: queryRemaining(simCard.PackageTotalFlowMb, simCard.UsedFlowMb),
|
||||
SmsService: simCard.SmsServiceStatus,
|
||||
PlanSMS: simCard.PackageTotalSms,
|
||||
UsedSMS: simCard.UsedSms,
|
||||
RemainingSMS: queryRemainingInt(simCard.PackageTotalSms, simCard.UsedSms),
|
||||
VoiceService: simCard.VoiceServiceStatus,
|
||||
PlanMinutes: simCard.PackageTotalVoice,
|
||||
UsedMinutes: simCard.UsedVoice,
|
||||
RemainingMinutes: queryRemainingInt(simCard.PackageTotalVoice, simCard.UsedVoice),
|
||||
InternationalRoamingService: simCard.RoamServiceStatus,
|
||||
PlanInternationalData: int(math.Round(simCard.PackageTotalRoamFlowMb)),
|
||||
UsedInternationalData: int(math.Round(simCard.UsedRoamFlowMb)),
|
||||
RemainingInternationalData: queryRemaining(simCard.PackageTotalRoamFlowMb, simCard.UsedRoamFlowMb),
|
||||
Description: nil,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sQuerySim) QuerySimLimit(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimLimitOutput, err error) {
|
||||
simCard, err := s.querySim(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trafficCutoffName := "删除"
|
||||
if simCard.TrafficCutoffStatus == 1 {
|
||||
trafficCutoffName = "已开启"
|
||||
} else if simCard.TrafficCutoffStatus == 2 {
|
||||
trafficCutoffName = "已关闭"
|
||||
}
|
||||
output = &model.QuerySimLimitOutput{
|
||||
TrafficCutoffStatus: simCard.TrafficCutoffStatus,
|
||||
TrafficCutoffName: trafficCutoffName,
|
||||
TrafficCutoffThreshold: simCard.TrafficCutoffThreshold,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sQuerySim) QuerySimImei(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimImeiOutput, err error) {
|
||||
simCard, err := s.querySim(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if imei := strings.TrimSpace(simCard.Imei); imei != "" {
|
||||
return &model.QuerySimImeiOutput{Imei: imei}, nil
|
||||
}
|
||||
|
||||
upstreamInput := &model.SimImeiInput{
|
||||
SimKeys: []*model.SimKey{{
|
||||
ChanKey: strings.TrimSpace(simCard.ChanKey),
|
||||
ICCID: strings.TrimSpace(simCard.Iccid),
|
||||
IMSI: strings.TrimSpace(simCard.Imsi),
|
||||
MSISDN: strings.TrimSpace(simCard.Msisdn),
|
||||
}},
|
||||
}
|
||||
upstreamResults, err := service.UpstreamChan().QuerySimImei(ctx, upstreamInput)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "QuerySim.QuerySimImei upstream query failed, iccid: %s, err: %v", simCard.Iccid, err)
|
||||
return &model.QuerySimImeiOutput{Imei: ""}, nil
|
||||
}
|
||||
for _, item := range upstreamResults {
|
||||
if item.Code != 0 {
|
||||
continue
|
||||
}
|
||||
if imei := strings.TrimSpace(item.ResData.Imei); imei != "" {
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
// imei_update_time 记录"值实际变更"的时间(单卡回填必然是从空到有值,
|
||||
// 故一定发生变更);imei_sync_time 专属于 worker 轮询链路,
|
||||
// 此处不写,避免污染"轮询覆盖度"指标。
|
||||
if _, updateErr := service.IotTSimCardInfo().
|
||||
Model(ctx).
|
||||
Where(columns.Id, simCard.Id).
|
||||
Data(g.Map{
|
||||
columns.Imei: imei,
|
||||
columns.ImeiUpdateTime: gtime.Now(),
|
||||
}).
|
||||
Update(); updateErr != nil {
|
||||
g.Log().Warningf(ctx, "QuerySim.QuerySimImei persist upstream imei failed, card id: %d, err: %v", simCard.Id, updateErr)
|
||||
}
|
||||
return &model.QuerySimImeiOutput{Imei: imei}, nil
|
||||
}
|
||||
}
|
||||
|
||||
output = &model.QuerySimImeiOutput{
|
||||
Imei: "",
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sQuerySim) QuerySimUsage(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimUsageOutput, err error) {
|
||||
simCard, err := s.querySim(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.QuerySimUsageOutput{
|
||||
UseUnit: "MB",
|
||||
UseAmount: simCard.UsedFlowMb,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// QuerySimStatus 查询SIM卡状态(纯读本地库,不调用上游通道)
|
||||
func (s *sQuerySim) QuerySimStatus(ctx context.Context, input *model.QuerySimInput) (output *model.QuerySimCardStatusOutput, err error) {
|
||||
simCard, err := s.querySim(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statusName, ok := consts.V3ApiSimBossCardStatusNameMap[simCard.BossCardStatus]
|
||||
if !ok {
|
||||
statusName = "未知"
|
||||
}
|
||||
output = &model.QuerySimCardStatusOutput{
|
||||
Iccid: simCard.Iccid,
|
||||
LifeCycle: simCard.BossCardStatus,
|
||||
LifeCycleName: statusName,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/bcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/utils"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const ydOperatorTokenURL = "https://api.iot.10086.cn/v5/ec/get/token"
|
||||
|
||||
type ydOperatorTokenResp struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
Token string `json:"token"`
|
||||
Ttl int64 `json:"ttl"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
var ydOperatorTokenSeq atomic.Int64
|
||||
|
||||
type sQueryToken struct {
|
||||
}
|
||||
|
||||
func NewQueryToken() *sQueryToken {
|
||||
return &sQueryToken{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterQueryToken(NewQueryToken())
|
||||
}
|
||||
|
||||
// Token 获取Token
|
||||
func (l *sQueryToken) Token(ctx context.Context, input model.TokenInputApi) (res model.TokenOutputApi, err error) {
|
||||
// 查询API集成信息
|
||||
var tApi entity.TApi
|
||||
err = dao.TApi.Ctx(ctx).Where(dao.TApi.Columns().AppKey, input.AppKey).Scan(&tApi)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "QueryToken.Token, Select t api by app key failed, err: %v, input: %v", err, input)
|
||||
return res, gerror.New("APPKey或APPSecret错误")
|
||||
}
|
||||
// 校验APPKey是否存在和APPSecret是否正确
|
||||
if tApi.Id == 0 {
|
||||
g.Log().Debugf(ctx, "QueryToken.Token, The t api does not exist, input: %v", input)
|
||||
return res, gerror.New("APPKey或APPSecret错误")
|
||||
}
|
||||
if tApi.AppSecret != input.AppSecret {
|
||||
g.Log().Debugf(ctx, "QueryToken.Token, APPSecret verification failed, input: %v", input)
|
||||
return res, gerror.New("APPKey或APPSecret错误")
|
||||
}
|
||||
if tApi.Status == 1 {
|
||||
g.Log().Debugf(ctx, "QueryToken.Token, The t api is disabled, input: %v", input)
|
||||
return res, gerror.New("API集成已停用")
|
||||
}
|
||||
// 验证客户
|
||||
// dept, err := service.SysDept().GetDeptById(ctx, tApi.DeptId)
|
||||
var dept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).Where(dao.SysDept.Columns().DeptId, tApi.DeptId).Scan(&dept)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "QueryToken.Token, Select dept by id failed, err: %v, input: %v", err, input)
|
||||
return res, gerror.New("APPKey或APPSecret错误")
|
||||
}
|
||||
if dept.DeptId == 0 {
|
||||
g.Log().Debugf(ctx, "QueryToken.Token, The organization does not exist, input: %v", input)
|
||||
return res, gerror.New("APPKey或APPSecret错误")
|
||||
}
|
||||
g.Log().Debugf(ctx, "dept.DeptId: %v", dept.DeptId)
|
||||
if dept.DeptKey == "" {
|
||||
g.Log().Debugf(ctx, "QueryToken.Token, The organization does not have API service enabled, input: %v", input)
|
||||
return res, gerror.New("客户未开启API服务")
|
||||
}
|
||||
|
||||
identity := &model.CustomClaimsApi{
|
||||
BaseClaimsApi: model.BaseClaimsApi{
|
||||
UUID: strings.ReplaceAll(uuid.New().String(), "-", ""),
|
||||
ApiId: tApi.Id,
|
||||
AppKey: tApi.AppKey,
|
||||
GroupId: tApi.GroupId,
|
||||
DeptKey: dept.DeptKey,
|
||||
DeptName: dept.DeptName,
|
||||
DeptType: dept.DeptType,
|
||||
TenantId: dept.TenantId,
|
||||
DeptId: dept.DeptId,
|
||||
LoginAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
contextModel := new(model.Context)
|
||||
ctx = contexts.Set(ctx, contextModel)
|
||||
|
||||
if input.Refresh == "" {
|
||||
input.Refresh = "1"
|
||||
} else if input.Refresh != "0" && input.Refresh != "1" {
|
||||
return res, gerror.New("刷新参数错误")
|
||||
}
|
||||
|
||||
res.AppId = dept.DeptKey
|
||||
if input.Refresh == "0" {
|
||||
tokenUuid, cacheErr := bcache.GetApiAuthActiveToken(ctx, dept.DeptId)
|
||||
if cacheErr == nil && tokenUuid != "" {
|
||||
token, tokenErr := l.GenerateShortToken(ctx, tokenUuid)
|
||||
if tokenErr != nil {
|
||||
return res, tokenErr
|
||||
}
|
||||
res.AccessToken = token
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
if input.Refresh == "1" {
|
||||
tokenUuid, cacheErr := bcache.GetApiAuthActiveToken(ctx, dept.DeptId)
|
||||
if cacheErr == nil && tokenUuid != "" {
|
||||
_ = bcache.DelApiAuthToken(ctx, tokenUuid)
|
||||
}
|
||||
_ = bcache.DelApiAuthActiveToken(ctx, dept.DeptId)
|
||||
}
|
||||
|
||||
_, token, err := l.GenerateToken(ctx, identity)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.AccessToken = token
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// OperatorToken 获取移动运营商Token(仅需chanKey,不校验白名单和AppId)
|
||||
func (l *sQueryToken) OperatorToken(ctx context.Context, input model.OperatorTokenInput) (res model.OperatorTokenOutput, err error) {
|
||||
return l.fetchYdOperatorToken(ctx, input, false)
|
||||
}
|
||||
|
||||
// OperatorTokenWithAuth 获取移动运营商Token(校验白名单+AppId,需传appId+chanKey)
|
||||
func (l *sQueryToken) OperatorTokenWithAuth(ctx context.Context, input model.OperatorTokenInput) (res model.OperatorTokenOutput, err error) {
|
||||
if input.AppId == "" {
|
||||
return res, gerror.New("AppId不能为空")
|
||||
}
|
||||
return l.fetchYdOperatorToken(ctx, input, true)
|
||||
}
|
||||
|
||||
// fetchYdOperatorToken 内部实现,checkShare=true时校验白名单和AppId
|
||||
func (l *sQueryToken) fetchYdOperatorToken(ctx context.Context, input model.OperatorTokenInput, checkShare bool) (res model.OperatorTokenOutput, err error) {
|
||||
if input.ChanKey == "" {
|
||||
return res, gerror.New("通道Key不能为空")
|
||||
}
|
||||
|
||||
fields := []any{
|
||||
dao.TSimChannels.Columns().TempKey,
|
||||
dao.TSimChannels.Columns().ChanAlias,
|
||||
dao.TSimChannels.Columns().ChanKey,
|
||||
dao.TSimChannels.Columns().AppKey,
|
||||
dao.TSimChannels.Columns().AppSecret,
|
||||
}
|
||||
if checkShare {
|
||||
fields = append(fields, dao.TSimChannels.Columns().ShareEnabled)
|
||||
}
|
||||
|
||||
var simChannels entity.TSimChannels
|
||||
dbErr := dao.TSimChannels.DB().Model(dao.TSimChannels.Table()).Safe().Ctx(ctx).
|
||||
Fields(fields...).
|
||||
Where(dao.TSimChannels.Columns().ChanKey, input.ChanKey).
|
||||
Scan(&simChannels)
|
||||
if dbErr != nil {
|
||||
g.Log().Errorf(ctx, "QueryToken.fetchYdOperatorToken, Select sim channel by chan key failed, err: %v, input: %v", dbErr, input)
|
||||
return res, gerror.New("通道不存在")
|
||||
}
|
||||
if simChannels.ChanKey == "" {
|
||||
return res, gerror.New("通道不存在")
|
||||
}
|
||||
if checkShare {
|
||||
if simChannels.ShareEnabled != 1 {
|
||||
g.Log().Warningf(ctx, "QueryToken.fetchYdOperatorToken, sim channel share not enabled, chanKey: %s", input.ChanKey)
|
||||
return res, gerror.New("该通道未开放共享Token")
|
||||
}
|
||||
if input.AppId != simChannels.AppKey {
|
||||
g.Log().Warningf(ctx, "QueryToken.fetchYdOperatorToken, appId mismatch, chanKey: %s, inputAppId: %s, channelAppId: %s", input.ChanKey, input.AppId, simChannels.AppKey)
|
||||
return res, gerror.New("AppId与通道不匹配")
|
||||
}
|
||||
}
|
||||
|
||||
var tempModel string
|
||||
if simChannels.TempKey != "" {
|
||||
var template entity.TSimChannelTemplate
|
||||
if tmplErr := dao.TSimChannelTemplate.Ctx(ctx).
|
||||
Fields(dao.TSimChannelTemplate.Columns().TempModel).
|
||||
Where(dao.TSimChannelTemplate.Columns().TempKey, simChannels.TempKey).
|
||||
Limit(1).Scan(&template); tmplErr == nil {
|
||||
tempModel = strings.TrimSpace(template.TempModel)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.EqualFold(tempModel, "shared_token") {
|
||||
result, sharedErr := service.UpstreamChan().GetSharedToken(ctx, input.ChanKey)
|
||||
if sharedErr != nil {
|
||||
return res, gerror.Wrap(sharedErr, "获取共享Token失败")
|
||||
}
|
||||
return model.OperatorTokenOutput{
|
||||
ChanAlias: result.ChanAlias,
|
||||
ChanKey: result.ChanKey,
|
||||
AccessToken: result.AccessToken,
|
||||
AccessTokenExpire: result.AccessTokenExpire,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if simChannels.AppKey == "" || simChannels.AppSecret == "" {
|
||||
return res, gerror.New("通道不存在")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
transID := buildYdOperatorTokenTransID(simChannels.AppKey, now)
|
||||
params := g.Map{
|
||||
"appid": simChannels.AppKey,
|
||||
"password": simChannels.AppSecret,
|
||||
"transid": transID,
|
||||
"refresh": "0",
|
||||
}
|
||||
g.Log().Infof(ctx, "QueryToken.OperatorToken, request yd token url: %s, chanKey: %s", buildYdOperatorTokenRequestURL(simChannels.AppKey, simChannels.AppSecret, transID), input.ChanKey)
|
||||
client := g.Client()
|
||||
client.SetTimeout(10 * time.Second)
|
||||
resp, err := client.Get(ctx, ydOperatorTokenURL, params)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "QueryToken.OperatorToken, request yd token failed, err: %v, chanKey: %s", err, input.ChanKey)
|
||||
return res, gerror.New("获取移动运营商Token失败")
|
||||
}
|
||||
|
||||
body := resp.ReadAllString()
|
||||
var ydResp ydOperatorTokenResp
|
||||
if err = json.Unmarshal([]byte(body), &ydResp); err != nil {
|
||||
g.Log().Errorf(ctx, "QueryToken.OperatorToken, parse yd token response failed, err: %v, chanKey: %s, body: %s", err, input.ChanKey, body)
|
||||
return res, gerror.New("解析移动运营商Token响应失败")
|
||||
}
|
||||
if ydResp.Status != "0" {
|
||||
g.Log().Warningf(ctx, "QueryToken.OperatorToken, yd token response error, status: %s, message: %s, chanKey: %s", ydResp.Status, ydResp.Message, input.ChanKey)
|
||||
if ydResp.Message != "" {
|
||||
return res, gerror.New(ydResp.Message)
|
||||
}
|
||||
return res, gerror.New("获取移动运营商Token失败")
|
||||
}
|
||||
if len(ydResp.Result) == 0 || ydResp.Result[0].Token == "" {
|
||||
return res, gerror.New("移动运营商Token为空")
|
||||
}
|
||||
if ydResp.Result[0].Ttl <= 0 {
|
||||
return res, gerror.New("移动运营商Token有效期无效")
|
||||
}
|
||||
|
||||
return model.OperatorTokenOutput{
|
||||
ChanAlias: simChannels.ChanAlias,
|
||||
ChanKey: simChannels.ChanKey,
|
||||
AccessToken: ydResp.Result[0].Token,
|
||||
AccessTokenExpire: gtime.NewFromTime(now.Add(time.Duration(ydResp.Result[0].Ttl) * time.Second)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildYdOperatorTokenTransID(appKey string, now time.Time) string {
|
||||
return appKey + now.Format("20060102150405") + fmt.Sprintf("%08d", ydOperatorTokenSeq.Add(1)%100000000)
|
||||
}
|
||||
|
||||
func buildYdOperatorTokenRequestURL(appKey, password, transID string) string {
|
||||
values := url.Values{}
|
||||
values.Set("appid", appKey)
|
||||
values.Set("password", password)
|
||||
values.Set("transid", transID)
|
||||
values.Set("refresh", "0")
|
||||
return ydOperatorTokenURL + "?" + values.Encode()
|
||||
}
|
||||
|
||||
// GenerateToken 生成Token
|
||||
func (s *sQueryToken) GenerateToken(ctx context.Context, customer *model.CustomClaimsApi) (claims *model.CustomClaimsApi, shortToken string, err error) {
|
||||
ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "3650d").String()
|
||||
bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String()
|
||||
iss := g.Cfg().MustGet(ctx, "jwt.issuer", "JieIOTAPI").String()
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String()
|
||||
et, _ := utils.ParseDuration(ets)
|
||||
bt, _ := utils.ParseDuration(bts)
|
||||
g.Log().Infof(ctx, "GenerateToken: customer:%+v, et:%s, bt:%s, iss:%s", customer, et, bt, iss)
|
||||
|
||||
tokenUuid := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
expiresAt := time.Now().Add(et).Unix()
|
||||
claims = &model.CustomClaimsApi{
|
||||
BaseClaimsApi: model.BaseClaimsApi{
|
||||
UUID: tokenUuid,
|
||||
ApiId: customer.ApiId,
|
||||
AppKey: customer.AppKey,
|
||||
GroupId: customer.GroupId,
|
||||
DeptKey: customer.DeptKey,
|
||||
DeptName: customer.DeptName,
|
||||
DeptType: customer.DeptType,
|
||||
DeptId: customer.DeptId,
|
||||
TenantId: customer.TenantId,
|
||||
LoginAt: time.Now().Unix(),
|
||||
},
|
||||
BufferTime: int64(bt / time.Second),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"JIEIOT"},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Unix(expiresAt, 0)),
|
||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)),
|
||||
Issuer: iss,
|
||||
},
|
||||
}
|
||||
|
||||
fullToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
fullTokenString, err := fullToken.SignedString([]byte(sk))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err = bcache.SetApiAuthToken(ctx, tokenUuid, fullTokenString, et); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err = bcache.SetApiAuthActiveToken(ctx, customer.DeptId, tokenUuid, et); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
shortToken, err = s.GenerateShortToken(ctx, tokenUuid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return claims, shortToken, nil
|
||||
}
|
||||
|
||||
// GenerateShortToken 生成短令牌
|
||||
func (s *sQueryToken) GenerateShortToken(ctx context.Context, tokenUuid string) (token string, err error) {
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String()
|
||||
shortToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &model.ApiTokenClaims{
|
||||
UUID: tokenUuid,
|
||||
})
|
||||
delete(shortToken.Header, "typ")
|
||||
return shortToken.SignedString([]byte(sk))
|
||||
}
|
||||
|
||||
// 解析Token
|
||||
func (s *sQueryToken) ParseToken(ctx context.Context, token string) (claims *model.CustomClaimsApi, err error) {
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String()
|
||||
t, err := jwt.ParseWithClaims(token, &model.ApiTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(sk), nil
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "sApiAuth.ParseToken short token err: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
shortClaims, ok := t.Claims.(*model.ApiTokenClaims)
|
||||
if !ok || !t.Valid {
|
||||
return nil, errors.New("token invalid")
|
||||
}
|
||||
|
||||
fullTokenString, err := bcache.GetApiAuthToken(ctx, shortClaims.UUID)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "sApiAuth.ParseToken get full token err: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
fullToken, err := jwt.ParseWithClaims(fullTokenString, &model.CustomClaimsApi{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(sk), nil
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "sApiAuth.ParseToken full token err: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
claims, ok = fullToken.Claims.(*model.CustomClaimsApi)
|
||||
if !ok || !fullToken.Valid {
|
||||
return nil, errors.New("token invalid")
|
||||
}
|
||||
if claims.ExpiresAt == nil || claims.ExpiresAt.Unix() < time.Now().Unix() {
|
||||
return nil, errors.New("token expired")
|
||||
}
|
||||
if claims.UUID != shortClaims.UUID {
|
||||
return nil, errors.New("token invalid")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// 删除Token
|
||||
func (s *sQueryToken) DeleteToken(ctx context.Context, token string) (err error) {
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String()
|
||||
t, err := jwt.ParseWithClaims(token, &model.ApiTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(sk), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shortClaims, ok := t.Claims.(*model.ApiTokenClaims)
|
||||
if !ok {
|
||||
return errors.New("token invalid")
|
||||
}
|
||||
return bcache.DelApiAuthToken(ctx, shortClaims.UUID)
|
||||
}
|
||||
|
||||
// 获取token
|
||||
func (s *sQueryToken) GetAccessToken(ctx context.Context) (token string, err error) {
|
||||
headerToken := g.RequestFromCtx(ctx).Header.Get("X-Access-Token")
|
||||
if headerToken == "" {
|
||||
return "", gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
g.Log().Error(ctx, headerToken)
|
||||
token = strings.TrimPrefix(headerToken, "Bearer ")
|
||||
if token == "" {
|
||||
token = headerToken
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// 根据Token获取当前登录客户信息
|
||||
func (s *sQueryToken) GetCurrentCustomer(ctx context.Context) (claims *model.CustomClaimsApi, err error) {
|
||||
token, err := s.GetAccessToken(ctx)
|
||||
g.Log().Debugf(ctx, "sQueryToken.GetCurrentCustomer token: %+v, err: %+v", token, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, err = s.ParseToken(ctx, token)
|
||||
g.Log().Debugf(ctx, "sApiAuth.GetCurrentCustomer claims: %+v, err: %+v", claims, err)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "sApiAuth.GetCurrentCustomer err: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package query
|
||||
|
||||
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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
)
|
||||
|
||||
const queryTrafficCutoffConfigMaxCards = 1000
|
||||
|
||||
type trafficCutoffIdentifier struct {
|
||||
Msisdn string
|
||||
Iccid string
|
||||
Imsi string
|
||||
}
|
||||
|
||||
func validateTrafficCutoffIdentifier(item trafficCutoffIdentifier) error {
|
||||
count := 0
|
||||
if item.Msisdn != "" {
|
||||
count++
|
||||
}
|
||||
if item.Iccid != "" {
|
||||
count++
|
||||
}
|
||||
if item.Imsi != "" {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("msisdn、iccid、imsi至少传一个")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trafficCutoffIdentifierKey(item trafficCutoffIdentifier) string {
|
||||
if item.Iccid != "" {
|
||||
return "iccid:" + item.Iccid
|
||||
}
|
||||
if item.Msisdn != "" {
|
||||
return "msisdn:" + item.Msisdn
|
||||
}
|
||||
return "imsi:" + item.Imsi
|
||||
}
|
||||
|
||||
func trafficCutoffCardMatchesIdentifier(card *entity.TSimCardInfo, identifier trafficCutoffIdentifier) 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 *sQuerySim) QueryTrafficCutoffConfig(ctx context.Context, in *model.QueryTrafficCutoffConfigParam) (*model.QueryTrafficCutoffConfigOutput, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("查询参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.AppId) == "" {
|
||||
return nil, gerror.New("appId不能为空")
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return nil, gerror.New("查询卡不能为空")
|
||||
}
|
||||
if len(in.Cards) > queryTrafficCutoffConfigMaxCards {
|
||||
return nil, gerror.Newf("单次最多查询%d张卡", queryTrafficCutoffConfigMaxCards)
|
||||
}
|
||||
|
||||
identifiers := make([]trafficCutoffIdentifier, 0, len(in.Cards))
|
||||
seen := make(map[string]struct{}, len(in.Cards))
|
||||
for _, item := range in.Cards {
|
||||
identifier := trafficCutoffIdentifier{Msisdn: strings.TrimSpace(item.Msisdn), Iccid: strings.TrimSpace(item.Iccid), Imsi: strings.TrimSpace(item.Imsi)}
|
||||
if err := validateTrafficCutoffIdentifier(identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := trafficCutoffIdentifierKey(identifier)
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, gerror.Newf("卡片标识重复:%s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
identifiers = append(identifiers, identifier)
|
||||
}
|
||||
|
||||
cardMap, err := loadTrafficCutoffCards(ctx, identifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := &model.QueryTrafficCutoffConfigOutput{Total: len(identifiers), Items: make([]model.QueryTrafficCutoffConfigItem, 0, len(identifiers))}
|
||||
for _, identifier := range identifiers {
|
||||
result := model.QueryTrafficCutoffConfigItem{}
|
||||
card := cardMap[trafficCutoffIdentifierKey(identifier)]
|
||||
if !trafficCutoffCardMatchesIdentifier(card, identifier) {
|
||||
result.ErrorCode = "CARD_NOT_FOUND"
|
||||
result.ErrorMessage = "卡片不存在或无权限"
|
||||
output.NotFoundCount++
|
||||
output.Items = append(output.Items, result)
|
||||
continue
|
||||
}
|
||||
result.Matched = true
|
||||
result.Iccid = strings.TrimSpace(card.Iccid)
|
||||
result.TrafficCutoffStatus = card.TrafficCutoffStatus
|
||||
result.TrafficCutoffStatusName = trafficCutoffStatusName(card.TrafficCutoffStatus)
|
||||
result.TrafficCutoffThreshold = card.TrafficCutoffThreshold
|
||||
if result.TrafficCutoffStatus == 0 {
|
||||
result.TrafficCutoffThreshold = 0
|
||||
}
|
||||
result.TrafficCutoffThresholdUnit = "MB"
|
||||
if in.IncludeUsage {
|
||||
packageTotalFlowMb := card.PackageTotalFlowMb
|
||||
usedFlowMb := card.UsedFlowMb
|
||||
remainingFlowMb := maxFloat(packageTotalFlowMb-usedFlowMb, 0)
|
||||
result.PackageTotalFlowMb = &packageTotalFlowMb
|
||||
result.UsedFlowMb = &usedFlowMb
|
||||
result.RemainingFlowMb = &remainingFlowMb
|
||||
}
|
||||
output.MatchedCount++
|
||||
output.Items = append(output.Items, result)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func loadTrafficCutoffCards(ctx context.Context, identifiers []trafficCutoffIdentifier) (map[string]*entity.TSimCardInfo, error) {
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
result := make(map[string]*entity.TSimCardInfo, len(identifiers))
|
||||
lookup := func(field string, values []string) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
var cards []*entity.TSimCardInfo
|
||||
err := service.IotTSimCardInfo().Model(ctx).
|
||||
Fields(columns.Iccid, columns.Msisdn, columns.Imsi, columns.PackageTotalFlowMb, columns.UsedFlowMb, columns.TrafficCutoffStatus, columns.TrafficCutoffThreshold).
|
||||
WhereIn(field, values).Scan(&cards)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "查询达量断网配置失败")
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card == nil {
|
||||
continue
|
||||
}
|
||||
if card.Iccid != "" {
|
||||
result["iccid:"+strings.TrimSpace(card.Iccid)] = card
|
||||
}
|
||||
if card.Msisdn != "" {
|
||||
result["msisdn:"+strings.TrimSpace(card.Msisdn)] = card
|
||||
}
|
||||
if card.Imsi != "" {
|
||||
result["imsi:"+strings.TrimSpace(card.Imsi)] = card
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
iccidValues, msisdnValues, imsiValues := make([]string, 0), make([]string, 0), make([]string, 0)
|
||||
for _, item := range identifiers {
|
||||
switch {
|
||||
case item.Iccid != "":
|
||||
iccidValues = append(iccidValues, item.Iccid)
|
||||
case item.Msisdn != "":
|
||||
msisdnValues = append(msisdnValues, item.Msisdn)
|
||||
default:
|
||||
imsiValues = append(imsiValues, item.Imsi)
|
||||
}
|
||||
}
|
||||
if err := lookup(columns.Iccid, iccidValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lookup(columns.Msisdn, msisdnValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lookup(columns.Imsi, imsiValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func trafficCutoffStatusName(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "已开启"
|
||||
case 2:
|
||||
return "已关闭"
|
||||
default:
|
||||
return "删除"
|
||||
}
|
||||
}
|
||||
|
||||
func maxFloat(value, floor float64) float64 {
|
||||
if value < floor {
|
||||
return floor
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/net/gsvc"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sUpstreamChan struct {
|
||||
}
|
||||
|
||||
func setClientTimeout(client *gclient.Client, timeout time.Duration) {
|
||||
client.SetTimeout(timeout)
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
func NewUpstreamChan() *sUpstreamChan {
|
||||
return &sUpstreamChan{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterUpstreamChan(NewUpstreamChan())
|
||||
}
|
||||
|
||||
func (s *sUpstreamChan) GetUpstreamChanConfig(ctx context.Context) (output model.UpstreamChanConfig, err error) {
|
||||
// 获取配置文件信息
|
||||
proxyConfig, err := g.Cfg().Get(ctx, "upstream.proxy")
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
proxyConfig.Scan(&output)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sUpstreamChan) getGatewayConfig(ctx context.Context) (output model.UpstreamChanConfig, err error) {
|
||||
gatewayConfig, err := g.Cfg().Get(ctx, "upstream.gateway")
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
gatewayConfig.Scan(&output)
|
||||
if output.Address == "" {
|
||||
return output, gerror.New("upstream gateway config not found")
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
type sharedTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data *sharedTokenResponseData `json:"data"`
|
||||
}
|
||||
|
||||
type sharedTokenResponseData struct {
|
||||
ChanAlias string `json:"chanAlias"`
|
||||
ChanKey string `json:"chanKey"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
AccessTokenExpire string `json:"accessTokenExpire"`
|
||||
}
|
||||
|
||||
func (s *sUpstreamChan) GetSharedToken(ctx context.Context, chanKey string) (*model.SharedTokenResult, error) {
|
||||
chanConfig, err := s.getGatewayConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.GetSharedToken, get gateway config error %+v", err)
|
||||
return nil, gerror.Wrap(err, "获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout)
|
||||
url := fmt.Sprintf("http://%s/internal/channel/shared-token", chanConfig.Address)
|
||||
resp, err := client.Post(ctx, url, g.Map{"chanKey": chanKey})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.GetSharedToken, post error %+v, url %s, chanKey %s", err, url, chanKey)
|
||||
return nil, gerror.New("请求共享Token失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
var output sharedTokenResponse
|
||||
if err = json.Unmarshal(body, &output); err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.GetSharedToken, unmarshal error %+v, url %s, body %s", err, url, body)
|
||||
return nil, gerror.New("解析共享Token响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
if output.Message == "" {
|
||||
output.Message = "获取共享Token失败"
|
||||
}
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
if output.Data == nil {
|
||||
return nil, gerror.New("共享Token响应数据为空")
|
||||
}
|
||||
result := &model.SharedTokenResult{
|
||||
ChanAlias: output.Data.ChanAlias,
|
||||
ChanKey: output.Data.ChanKey,
|
||||
AccessToken: output.Data.AccessToken,
|
||||
}
|
||||
if output.Data.AccessTokenExpire != "" {
|
||||
if t, parseErr := gtime.StrToTime(output.Data.AccessTokenExpire); parseErr == nil {
|
||||
result.AccessTokenExpire = t
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获取卡基本信息
|
||||
func (s *sUpstreamChan) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) ([]model.SimBaseRes[model.SimBasicInfoItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimBasicInfo, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-basic-info", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-basic-info")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimBasicInfo, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimBasicInfoOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimBasicInfo, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimBasicInfo, query sim basic info error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var basicInfo []model.SimBaseRes[model.SimBasicInfoItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimBasicInfo, query sim basic info error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
basicInfo = append(basicInfo, item)
|
||||
}
|
||||
return basicInfo, nil
|
||||
}
|
||||
|
||||
// 查询会话信息
|
||||
func (s *sUpstreamChan) QuerySimSession(ctx context.Context, input *model.SimSessionInput) ([]model.SimBaseRes[model.SimSessionItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimSession, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-session", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout)
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-session")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimSession, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimSessionOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimSession, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimSession, query sim session error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var session []model.SimBaseRes[model.SimSessionItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimSession, query sim session error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
session = append(session, item)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// 查询状态
|
||||
func (s *sUpstreamChan) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardStatus, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-card-status", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-status")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardStatus, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimCardStatusOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardStatus, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimCardStatus, query sim card status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var cardStatus []model.SimBaseRes[model.SimCardStatusItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimCardStatus, query sim card status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
cardStatus = append(cardStatus, item)
|
||||
}
|
||||
return cardStatus, nil
|
||||
}
|
||||
|
||||
// 查询IMEI
|
||||
func (s *sUpstreamChan) QuerySimImei(ctx context.Context, input *model.SimImeiInput) ([]model.SimBaseRes[model.SimImeiItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimImei, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-imei", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-imei")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimImei, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimImeiOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimImei, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimImei, query sim imei error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var imei []model.SimBaseRes[model.SimImeiItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimImei, query sim imei error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
imei = append(imei, item)
|
||||
}
|
||||
return imei, nil
|
||||
}
|
||||
|
||||
// 查询流量
|
||||
func (s *sUpstreamChan) QuerySimDatausage(ctx context.Context, input *model.SimDataUsageInput) ([]model.SimBaseRes[model.SimDataUsageItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDataUsage, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-datausage", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-datausage")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDataUsage, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimDataUsageOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDataUsage, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDataUsage, query sim data usage error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var dataUsage []model.SimBaseRes[model.SimDataUsageItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDataUsage, query sim datausage error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
dataUsage = append(dataUsage, item)
|
||||
}
|
||||
return dataUsage, nil
|
||||
}
|
||||
|
||||
// 查询月流量
|
||||
func (s *sUpstreamChan) QuerySimDatausageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) ([]model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageMonthly, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-datausage-monthly", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
g.Log().Debug(ctx, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-datausage-monthly")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageMonthly, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimDataUsageMonthlyOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageMonthly, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDatausageMonthly, query sim datausage monthly error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var dataUsage []model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDatausageMonthly, query sim datausage monthly error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
dataUsage = append(dataUsage, item)
|
||||
}
|
||||
return dataUsage, nil
|
||||
}
|
||||
|
||||
// 查询日流量
|
||||
func (s *sUpstreamChan) QuerySimDatausageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) ([]model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageDaily, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-datausage-daily", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-datausage-daily")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageDaily, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimDataUsageDailyOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDatausageDaily, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDatausageDaily, query sim datausage daily error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var dataUsage []model.SimBaseRes[model.SimDataUsageDailyOutputItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDatausageDaily, query sim datausage daily error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
dataUsage = append(dataUsage, item)
|
||||
}
|
||||
return dataUsage, nil
|
||||
}
|
||||
|
||||
// 查询已开通APN信息
|
||||
func (s *sUpstreamChan) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) ([]model.SimBaseRes[model.SimApnInfoItemList], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimApnInfo, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-apn-info", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-apn-info")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimApnInfo, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimApnInfoOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimApnInfo, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimApnInfo, query sim apn info error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var apnInfo []model.SimBaseRes[model.SimApnInfoItemList]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimApnInfo, query sim apn info error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
apnInfo = append(apnInfo, item)
|
||||
}
|
||||
return apnInfo, nil
|
||||
}
|
||||
|
||||
// 卡状态变更
|
||||
func (s *sUpstreamChan) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimStatus, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/change/sim-status", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/change/sim-status")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimStatus, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimCardStatusChangeOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimStatus, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.ChangeSimStatus, change sim status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var statusList []model.SimBaseRes[model.SimCardStatusChangeItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.ChangeSimStatus, change sim status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
}
|
||||
statusList = append(statusList, item)
|
||||
}
|
||||
return statusList, nil
|
||||
}
|
||||
|
||||
// 卡APN功能开停
|
||||
func (s *sUpstreamChan) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) ([]model.SimBaseRes[model.SimCardOperateChangeApnItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimApnFunction, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-apn", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-apn")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimApnFunction, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimCardOperateChangeApnOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.ChangeSimApnFunction, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.ChangeSimApnFunction, change sim apn function error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
var statusList []model.SimBaseRes[model.SimCardOperateChangeApnItem]
|
||||
for _, item := range output.Data {
|
||||
if item.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.ChangeSimApnFunction, change sim apn function error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
continue
|
||||
}
|
||||
statusList = append(statusList, item)
|
||||
}
|
||||
return statusList, nil
|
||||
}
|
||||
|
||||
// 查询机卡绑定状态
|
||||
func (s *sUpstreamChan) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) ([]model.SimBaseRes[model.SimCardBindStatusItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardBindStatus, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-card-bind-status", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-card-bind-status")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardBindStatus, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimCardBindStatusOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimCardBindStatus, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimCardBindStatus, query sim card bind status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
|
||||
// 查询设备状态
|
||||
func (s *sUpstreamChan) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) ([]model.SimBaseRes[model.SimDeviceStatusItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDeviceStatus, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-device-status", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-device-status")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDeviceStatus, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimDeviceStatusOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimDeviceStatus, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimDeviceStatus, query sim device status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
|
||||
// 查询地域限制状态
|
||||
func (s *sUpstreamChan) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) ([]model.SimBaseRes[model.SimRegionLimitStatusItem], error) {
|
||||
chanConfig, err := s.GetUpstreamChanConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimRegionLimitStatus, get upstream chan config error %+v, path %s, input %+v, ", err, "/sim/query/sim-region-limit-status", input)
|
||||
return nil, gerror.New("获取上游通道配置失败")
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
setClientTimeout(client, chanConfig.Timeout*time.Duration(len(input.SimKeys)))
|
||||
url := fmt.Sprintf("http://%s%s", chanConfig.Address, "/sim/query/sim-region-limit-status")
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimRegionLimitStatus, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
}
|
||||
body := resp.ReadAll()
|
||||
output := model.SimRegionLimitStatusOutput{}
|
||||
err = json.Unmarshal([]byte(body), &output)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamChan.QuerySimRegionLimitStatus, unmarshal error %+v,url %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New("解析响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
g.Log().Debugf(ctx, "sUpstreamChan.QuerySimRegionLimitStatus, query sim region limit status error %+v, path %s, params %+v, body %s, ", err, url, input, body)
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user