chore: 初始开源发布

iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。

- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
  RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
  LF 行尾 + .gitattributes
This commit is contained in:
MiFaZhan
2026-10-23 14:15:47 +08:00
commit 88dfe5c640
482 changed files with 34475 additions and 0 deletions
+426
View File
@@ -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
}