sync: 同步 OneLink 模拟服务 v1.0.4 并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:25:38 +08:00
parent 4e2cd43a55
commit 5299c13c1a
9 changed files with 170 additions and 58 deletions
+2 -16
View File
@@ -1,8 +1,6 @@
# iotlcm-onelink-mock
# OneLink 挡板服务
OneLink 挡板服务 —— 基于《中移物联网物联卡能力开放调用参考文档 V26.01.1》实现的本地测试替身。
用于在没有真实运营商环境时,模拟中国移动 OneLink 的鉴权与卡查询接口,支撑联调、自测与 CI。
基于《中移物联网物联卡能力开放调用参考文档 V26.01.1》实现的本地挡板服务。
## 功能
@@ -234,15 +232,3 @@ GET /api/orders?page=1&pageSize=20
```
Orders are persisted in the SQLite `mock_order` table and retain all nine fields after a service restart. Existing `mock_t_sim_card` data is preserved.
## 部署
本地开发用 `docker compose up -d --build` 即可(见上文「Docker 启动」)。若需要通过 Nginx 以子路径方式挂到已有站点,参考 [`docs/deploy.md`](./docs/deploy.md)。
## 相关项目
本服务是 [iotlcm](https://git.5iot.com/iotlcm-public) 物联网卡全生命周期管理平台的一部分,用于在没有真实运营商通道时提供可预期的上游替身。
## License
[Apache License 2.0](./LICENSE)
+8 -8
View File
@@ -623,7 +623,7 @@ paths:
required: false
description: Comma-separated MSISDNs to export. If omitted, all cards are exported.
schema: { type: string }
example: "1440292201313,138****8000"
example: "1440292201313,14700004176"
responses:
"200":
description: Excel file binary.
@@ -867,7 +867,7 @@ paths:
- simSessionList:
- apnId: CMIOT
status: "01"
ip: 192.0.2.42
ip: 127.0.0.1
ipv6Prefix: ""
ipv6: ""
createDate: "2026-06-06 14:00:00"
@@ -1438,7 +1438,7 @@ components:
description: Underscore-separated SIM identifiers. For batch endpoints, provide exactly one of msisdns, iccids, or imsis.
schema:
type: string
example: "1440292201313_138****8000"
example: "1440292201313_14700004176"
Iccids:
name: iccids
in: query
@@ -1526,7 +1526,7 @@ components:
- chanKey: channel_yd_mock
iccid: "8986040000000000001"
imsi: "460040000000000001"
msisdn: "138****8000"
msisdn: "14100000001"
reqData: {}
ChanSimKey:
type: object
@@ -1534,7 +1534,7 @@ components:
chanKey: { type: string, example: channel_yd_mock }
iccid: { type: string, example: "8986040000000000001" }
imsi: { type: string, example: "460040000000000001" }
msisdn: { type: string, example: "138****8000" }
msisdn: { type: string, example: "14100000001" }
ChanResponseList:
type: array
items:
@@ -1641,7 +1641,7 @@ components:
example: "01"
ip:
type: string
example: 192.0.2.42
example: 127.0.0.1
ipv6Prefix:
type: string
example: ""
@@ -2001,7 +2001,7 @@ components:
example: "2026-06-06 14:00:00"
ip:
type: string
example: 192.0.2.42
example: 127.0.0.1
ipv6Prefix:
type: string
example: ""
@@ -2213,7 +2213,7 @@ components:
onOffStatus: "1"
sessionStatus: "01"
sessionCreateDate: "2026-06-06 14:00:00"
ip: 192.0.2.42
ip: 127.0.0.1
ipv6Prefix: ""
ipv6: ""
rat: "6"
@@ -0,0 +1,88 @@
# SQLite Generated Cards Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the MySQL-backed card source with a local SQLite-backed mock data generator, fixed traffic values, and Excel export.
**Architecture:** The service will use SQLite as the runtime persistence layer and keep the existing in-memory `Store` shape for OneLink handlers. The admin sync action becomes card generation: it appends generated rows to SQLite and refreshes memory. Excel export maps stored card rows to the inbound workbook column format.
**Tech Stack:** Go `net/http`, `database/sql`, SQLite driver, Vue 3, Vite, simple XLSX generation via Go standard library zip/XML.
---
### Task 1: Lock Generated Card Rules With Tests
**Files:**
- Test: `internal/app/generator_test.go`
- Create: `internal/app/generator.go`
- [ ] Write tests that generated cards use timestamp-related unique identifiers, fixed monthly traffic `18432 KB`, APN `CMIOT`, and export metadata matching the confirmed sample.
- [ ] Run `go test ./internal/app -run TestGenerated -count=1` and verify it fails because the generator does not exist.
- [ ] Implement the minimal generator API and constants.
- [ ] Run the focused test and verify it passes.
### Task 2: Replace MySQL Backend With SQLite Backend
**Files:**
- Modify: `go.mod`
- Modify: `internal/app/store_file.go`
- Create/modify: `internal/app/store_sqlite.go`
- Modify: `internal/app/store.go`
- Test: `internal/app/store_test.go`
- [ ] Add tests that `NewStoreManager` no longer requires `BAFFLE_DB_DSN`, loads from SQLite, and appends generated cards without deleting existing rows.
- [ ] Run focused tests and verify failure.
- [ ] Implement SQLite schema creation, load, save, delete, and append generation behavior.
- [ ] Remove runtime database reload from `Snapshot()` so normal reads do not hit external DB logic.
- [ ] Run focused tests and verify pass.
### Task 3: Admin API And Excel Export
**Files:**
- Modify: `internal/app/admin.go`
- Create: `internal/app/export_xlsx.go`
- Test: `internal/app/admin_test.go` or existing app tests
- [ ] Add tests for `POST /api/generate` with `count`, and `GET /api/cards/export` returning XLSX bytes with the confirmed headers.
- [ ] Run focused tests and verify failure.
- [ ] Implement generate and export handlers.
- [ ] Run focused tests and verify pass.
### Task 4: Remove Traffic Growth From Runtime
**Files:**
- Modify: `internal/app/store.go`
- Modify: `internal/app/model.go`
- Modify: `internal/app/admin.go`
- Test: `internal/app/store_test.go`
- [ ] Add or update tests to prove snapshot does not increment traffic over time.
- [ ] Remove `applyGrowthLocked()` calls and UI-facing settings behavior.
- [ ] Keep fixed traffic values in generated card records.
- [ ] Run focused tests and verify pass.
### Task 5: Frontend Update
**Files:**
- Modify: `frontend/src/main.js`
- Modify: `frontend/src/style.css`
- [ ] Replace "一键同步" with "一键生成".
- [ ] Add generation count input.
- [ ] Call `POST /api/generate`.
- [ ] Add export button linking/downloading from `/api/cards/export`.
- [ ] Remove traffic growth controls and growth column.
- [ ] Run `npm run build` in `frontend`.
### Task 6: Documentation And Verification
**Files:**
- Modify: `README.md`
- Modify: `docker-compose.yaml`
- Modify: `docker-compose.yaml` if needed
- [ ] Update docs to describe SQLite, generation, export, and fixed traffic.
- [ ] Remove MySQL DSN instructions.
- [ ] Run `go test ./...` with a writable `GOCACHE`.
- [ ] Run frontend build.
- [ ] Check `git diff` for unintended changes.
+1 -27
View File
@@ -3,7 +3,6 @@ package service
import (
"fmt"
"strconv"
"sync"
"time"
)
@@ -51,31 +50,6 @@ var inboundExportHeaders = func() []string {
return headers
}()
// 生成序号分配器。
//
// 序号直接来自毫秒时间戳时,同一毫秒内的两次生成会得到完全相同的 seq,
// 进而生成重复的 ICCID/IMSI,触发 mock_t_sim_card 的唯一约束冲突
// (表现为「一键生成」连点两次或两个分组连续生成时报
// UNIQUE constraint failed: mock_t_sim_card.imsi)。
//
// 这里用进程内单调递增的上界保证 seq 严格递增:时间戳只决定起点,
// 真正的唯一性由 nextGeneratedSeq 保证。
var (
generatedSeqMu sync.Mutex
generatedSeqLast int64
)
// nextGeneratedSeq 返回严格大于上一次分配值的序号。
func nextGeneratedSeq(candidate int64) int64 {
generatedSeqMu.Lock()
defer generatedSeqMu.Unlock()
if candidate <= generatedSeqLast {
candidate = generatedSeqLast + 1
}
generatedSeqLast = candidate
return candidate
}
func GenerateSIMCards(batchTime time.Time, count int) []SIMCard {
if count <= 0 {
return []SIMCard{}
@@ -84,7 +58,7 @@ func GenerateSIMCards(batchTime time.Time, count int) []SIMCard {
base := batchTime.UnixMilli()
dbTime := batchTime.Format("2006-01-02 15:04:05")
for i := 0; i < count; i++ {
seq := nextGeneratedSeq(base + int64(i+1))
seq := base + int64(i+1)
card := SIMCard{
MSISDN: fmt.Sprintf("141%08d", seq%100000000),
ICCID: fmt.Sprintf("898604%014d", seq%100000000000000),
+1 -1
View File
@@ -50,7 +50,7 @@ func TestOrdersPersistAcrossStoreRestart(t *testing.T) {
t.Fatal(err)
}
want := Order{
OrderNum: "ORD202607270001", MSISDN: "138****8000", ICCID: "8986000000000000001",
OrderNum: "ORD202607270001", MSISDN: "14100000001", ICCID: "8986000000000000001",
IMSI: "460000000000001", Type: "sim-apn-function", OperType: "1",
APNName: "CMIOT", Status: "completed", CreatedAt: "2026-07-27 16:00:00",
}
+64
View File
@@ -0,0 +1,64 @@
param(
[Parameter(Mandatory = $true)]
[string]$Registry,
[string]$Namespace = 'iotlcm',
[string]$Tag = 'latest',
[string]$Username = $env:REGISTRY_USERNAME
)
$ErrorActionPreference = 'Stop'
$ProjectRoot = Resolve-Path (Join-Path $PSScriptRoot '..')
$Password = $env:REGISTRY_PASSWORD
if (-not [string]::IsNullOrWhiteSpace($Username) -and [string]::IsNullOrWhiteSpace($Password)) {
throw 'REGISTRY_PASSWORD is required when REGISTRY_USERNAME is set.'
}
function Invoke-Docker {
param(
[Parameter(Mandatory = $true)]
[string[]]$Arguments
)
& docker @Arguments
if ($LASTEXITCODE -ne 0) {
throw "docker $($Arguments -join ' ') failed with exit code $LASTEXITCODE"
}
}
$backendImage = "$Registry/$Namespace/onelink-mock-backend:$Tag"
$frontendImage = "$Registry/$Namespace/onelink-mock-frontend:$Tag"
Push-Location $ProjectRoot
try {
if (-not [string]::IsNullOrWhiteSpace($Username)) {
Write-Host "Logging in to $Registry..."
$Password | & docker login $Registry --username $Username --password-stdin
if ($LASTEXITCODE -ne 0) {
throw "docker login failed with exit code $LASTEXITCODE"
}
}
Write-Host "Building backend image: $backendImage"
Invoke-Docker @('build', '-t', $backendImage, '.')
Write-Host "Pushing backend image: $backendImage"
Invoke-Docker @('push', $backendImage)
Push-Location (Join-Path $ProjectRoot 'frontend')
try {
Write-Host "Building frontend image: $frontendImage"
Invoke-Docker @('build', '-t', $frontendImage, '.')
Write-Host "Pushing frontend image: $frontendImage"
Invoke-Docker @('push', $frontendImage)
} finally {
Pop-Location
}
Write-Host 'Image build and push completed.' -ForegroundColor Green
Write-Host " $backendImage"
Write-Host " $frontendImage"
} finally {
Pop-Location
}