refactor: rename iot client to stateless gateway
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
- 验收范围:`docs/12-验收与迭代规划.md`
|
- 验收范围:`docs/12-验收与迭代规划.md`
|
||||||
2. 检查 `git status`,保留用户已有改动;不要顺手格式化、重写或删除无关文件。
|
2. 检查 `git status`,保留用户已有改动;不要顺手格式化、重写或删除无关文件。
|
||||||
3. 区分三类内容:
|
3. 区分三类内容:
|
||||||
- `frontend/platform_admin`、`backend/{api,worker,iot-client,iot-server}` 是当前业务实现。
|
- `frontend/platform_admin`、`backend/{api,worker,iot-gateway,iot-server}` 是当前业务实现。
|
||||||
- `docs/10-技术实现规划.md` 中尚不存在的目录和系统属于规划,不得为完成局部任务擅自创建空壳工程。
|
- `docs/10-技术实现规划.md` 中尚不存在的目录和系统属于规划,不得为完成局部任务擅自创建空壳工程。
|
||||||
4. 需求不明确时以文档中的系统主责、数据主责和安全规则为边界;不得把标为“待确认”的事项自行固化为政策。
|
4. 需求不明确时以文档中的系统主责、数据主责和安全规则为边界;不得把标为“待确认”的事项自行固化为政策。
|
||||||
5. 文档之间出现冲突时按以下顺序处理:
|
5. 文档之间出现冲突时按以下顺序处理:
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
- `backend/api`:Gin HTTP API/BFF、鉴权、同步业务事务、数据库事实和审计。
|
- `backend/api`:Gin HTTP API/BFF、鉴权、同步业务事务、数据库事实和审计。
|
||||||
- `backend/worker`:Outbox 投递、Redis Streams 消费、告警、派单、通知、对账和超时任务。当前主要是 Mock 边界,未确定持久化消息契约前不要伪造完整实现。
|
- `backend/worker`:Outbox 投递、Redis Streams 消费、告警、派单、通知、对账和超时任务。当前主要是 Mock 边界,未确定持久化消息契约前不要伪造完整实现。
|
||||||
- `backend/iot-client`:管理系统/App 的设备上下行 HTTP 边界;不得绕过 API 的业务鉴权和安全状态校验。
|
- `backend/iot-gateway`:API/Worker 与 IoT Server 的无状态上下行 HTTP 轻网关;不得保存业务事实或绕过 API 的业务鉴权和安全状态校验。
|
||||||
- `backend/iot-server`:内嵌 Mochi MQTT Broker、厂商协议、遥测、设备命令和回执;本地配置不代表已完成真实设备联调。
|
- `backend/iot-server`:内嵌 Mochi MQTT Broker、厂商协议、遥测、设备命令和回执;本地配置不代表已完成真实设备联调。
|
||||||
- `frontend/platform_admin`:平台级治理、运营、财务、安全、主数据和审计界面;后端资源契约是其资源和操作能力的依据。
|
- `frontend/platform_admin`:平台级治理、运营、财务、安全、主数据和审计界面;后端资源契约是其资源和操作能力的依据。
|
||||||
- `docs`:产品和技术基线;涉及业务口径、实体、状态、权限或跨端流程的变更必须同步更新相应文档。
|
- `docs`:产品和技术基线;涉及业务口径、实体、状态、权限或跨端流程的变更必须同步更新相应文档。
|
||||||
@@ -60,7 +60,7 @@ platforms/
|
|||||||
backend/
|
backend/
|
||||||
api/ # 当前:同步 HTTP API/BFF
|
api/ # 当前:同步 HTTP API/BFF
|
||||||
worker/ # 当前:异步任务和事件消费
|
worker/ # 当前:异步任务和事件消费
|
||||||
iot-client/ # 当前:管理系统/App 的设备上下行接口
|
iot-gateway/ # 当前:API/Worker 与 IoT Server 的无状态上下行网关
|
||||||
iot-server/ # 当前:MQTT 与设备协议边界
|
iot-server/ # 当前:MQTT 与设备协议边界
|
||||||
contracts/
|
contracts/
|
||||||
openapi/ # 规划:HTTP 契约
|
openapi/ # 规划:HTTP 契约
|
||||||
@@ -147,7 +147,7 @@ go build ./cmd/main/main.go
|
|||||||
|
|
||||||
# Worker / IoT(改动对应模块时)
|
# Worker / IoT(改动对应模块时)
|
||||||
cd backend/worker && go test ./... && go build ./cmd/main/main.go
|
cd backend/worker && go test ./... && go build ./cmd/main/main.go
|
||||||
cd backend/iot-client && go test ./... && go build ./cmd/main/main.go
|
cd backend/iot-gateway && go test ./... && go build ./cmd/main/main.go
|
||||||
cd backend/iot-server && go test ./... && go build ./cmd/main/main.go
|
cd backend/iot-server && go test ./... && go build ./cmd/main/main.go
|
||||||
|
|
||||||
# 平台总后台
|
# 平台总后台
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `api` | Gin HTTP API / BFF、同步事务、JWT 鉴权与统一响应 |
|
| `api` | Gin HTTP API / BFF、同步事务、JWT 鉴权与统一响应 |
|
||||||
| `worker` | Outbox 投递、超时扫描与可重试异步任务 |
|
| `worker` | Outbox 投递、超时扫描与可重试异步任务 |
|
||||||
| `iot-client` | 管理系统/App 的设备上下行 HTTP 边界、幂等受理与状态查询 |
|
| `iot-gateway` | API/Worker 与 IoT Server 之间的无状态上下行 HTTP 轻网关 |
|
||||||
| `iot-server` | 内嵌 Mochi MQTT Broker、厂商二进制协议、遥测、命令与回执 |
|
| `iot-server` | 内嵌 Mochi MQTT Broker、厂商二进制协议、遥测、命令与回执 |
|
||||||
|
|
||||||
四个进程沿用统一的工程机制;API 是命令、Outbox 和上行报文的数据库事实主责,Worker 负责投递,IoT Client 负责系统接口,IoT Server 负责设备协议。
|
四个进程沿用统一的工程机制;API 是命令、Outbox 和上行报文的数据库事实主责,Worker 负责投递,IoT Client 负责系统接口,IoT Server 负责设备协议。
|
||||||
|
|||||||
@@ -50,4 +50,3 @@ Payment:
|
|||||||
|
|
||||||
IoT:
|
IoT:
|
||||||
InternalServiceToken: change-me-iot-internal-token
|
InternalServiceToken: change-me-iot-internal-token
|
||||||
ClientBaseURL: http://127.0.0.1:12429
|
|
||||||
|
|||||||
@@ -66,10 +66,9 @@ type PaymentConfig struct {
|
|||||||
Wechat WechatPayConfig `yaml:"Wechat"`
|
Wechat WechatPayConfig `yaml:"Wechat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IoTConfig 保存 API、Worker 与 IoT Client 间的内部认证和地址。
|
// IoTConfig 保存 API 与 IoT 链路间的内部认证配置。
|
||||||
type IoTConfig struct {
|
type IoTConfig struct {
|
||||||
InternalServiceToken string `yaml:"InternalServiceToken"`
|
InternalServiceToken string `yaml:"InternalServiceToken"`
|
||||||
ClientBaseURL string `yaml:"ClientBaseURL"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SrvConfig 与仓库现有进程的配置结构保持一致。
|
// SrvConfig 与仓库现有进程的配置结构保持一致。
|
||||||
@@ -110,8 +109,8 @@ func New(srvKey string) {
|
|||||||
if Spec.Payment.ExpireMinutes <= 0 || Spec.Payment.RefundWindowDays <= 0 {
|
if Spec.Payment.ExpireMinutes <= 0 || Spec.Payment.RefundWindowDays <= 0 {
|
||||||
panic("Payment expiration and refund window must be greater than zero")
|
panic("Payment expiration and refund window must be greater than zero")
|
||||||
}
|
}
|
||||||
if len(strings.TrimSpace(Spec.IoT.InternalServiceToken)) < 16 || !strings.HasPrefix(Spec.IoT.ClientBaseURL, "http") {
|
if len(strings.TrimSpace(Spec.IoT.InternalServiceToken)) < 16 {
|
||||||
panic("IoT internal token and client base URL are required")
|
panic("IoT internal token is required")
|
||||||
}
|
}
|
||||||
registerURL, err := url.ParseRequestURI(Spec.Global.UserRegisterURL)
|
registerURL, err := url.ParseRequestURI(Spec.Global.UserRegisterURL)
|
||||||
if err != nil || (registerURL.Scheme != "http" && registerURL.Scheme != "https") || registerURL.Host == "" {
|
if err != nil || (registerURL.Scheme != "http" && registerURL.Scheme != "https") || registerURL.Host == "" {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ go 1.26.1
|
|||||||
use (
|
use (
|
||||||
./api
|
./api
|
||||||
./worker
|
./worker
|
||||||
./iot-client
|
./iot-gateway
|
||||||
./iot-server
|
./iot-server
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,15 +2,26 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
|||||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||||
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
|
||||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||||
|
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||||
|
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
|
||||||
|
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||||
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
||||||
@@ -25,18 +36,24 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an
|
|||||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||||
|
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||||
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
|
||||||
|
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc=
|
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc=
|
||||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
||||||
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
|
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
|
||||||
@@ -46,6 +63,7 @@ github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBF
|
|||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
||||||
@@ -55,6 +73,7 @@ github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZd
|
|||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
@@ -67,8 +86,6 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
|||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
|
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||||
@@ -87,12 +104,16 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||||
|
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# IoT Client
|
|
||||||
|
|
||||||
管理系统与 App 的上下行设备接口边界。外部业务请求应先经过 Platform API 的 JWT、角色、对象归属和安全状态校验;API/Worker 使用内部令牌调用本服务。命令要求 `identity`、`idempotency_key`、过期时间,并返回可查询状态,不能把 HTTP 受理视为设备执行成功。
|
|
||||||
|
|
||||||
设备上行报文由 IoT Server 回调本服务,再转交 Platform API 持久化和审计。
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module git.apinb.com/heqiapp/platforms/backend/iot-client
|
|
||||||
|
|
||||||
go 1.26.1
|
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
|
||||||
5
backend/iot-gateway/README.md
Normal file
5
backend/iot-gateway/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# IoT Gateway
|
||||||
|
|
||||||
|
API/Worker 与 IoT Server 之间的无状态轻网关。外部业务请求必须先经过 Platform API 的 JWT、角色、对象归属和安全状态校验;Worker 使用内部令牌经本服务下发命令。网关不保存命令、幂等或设备事实,只透传 `identity`、`idempotency_key` 和过期时间,并返回 IoT Server 的派发结果。
|
||||||
|
|
||||||
|
设备上行报文由 IoT Server 回调本服务,再转交 Platform API 持久化和审计;命令与幂等事实、最终状态查询统一由 API/PostgreSQL 提供。
|
||||||
@@ -3,15 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/iot-client/internal/config"
|
"git.apinb.com/heqiapp/platforms/backend/iot-gateway/internal/config"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/iot-client/internal/service"
|
"git.apinb.com/heqiapp/platforms/backend/iot-gateway/internal/service"
|
||||||
"log"
|
"log"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
path := flag.String("config", "etc/platform_iot_client_dev.yaml", "YAML 配置文件")
|
path := flag.String("config", "etc/platform_iot_gateway_dev.yaml", "YAML 配置文件")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
cfg, err := config.Load(*path)
|
cfg, err := config.Load(*path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Service: platform-iot-client
|
Service: platform-iot-gateway
|
||||||
HTTP:
|
HTTP:
|
||||||
Address: 127.0.0.1:12429
|
Address: 127.0.0.1:12429
|
||||||
InternalToken: change-me-iot-internal-token
|
InternalToken: change-me-iot-internal-token
|
||||||
5
backend/iot-gateway/go.mod
Normal file
5
backend/iot-gateway/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.apinb.com/heqiapp/platforms/backend/iot-gateway
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -9,20 +9,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.apinb.com/heqiapp/platforms/backend/iot-client/internal/config"
|
"git.apinb.com/heqiapp/platforms/backend/iot-gateway/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg config.Config
|
cfg config.Config
|
||||||
client *http.Client
|
client *http.Client
|
||||||
mu sync.RWMutex
|
http *http.Server
|
||||||
byIdentity map[string]Command
|
|
||||||
byIdempotency map[string]string
|
|
||||||
http *http.Server
|
|
||||||
}
|
}
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Identity string `json:"identity"`
|
Identity string `json:"identity"`
|
||||||
@@ -37,11 +32,6 @@ type Command struct {
|
|||||||
Controller byte `json:"controller"`
|
Controller byte `json:"controller"`
|
||||||
Loop byte `json:"loop"`
|
Loop byte `json:"loop"`
|
||||||
Component byte `json:"component"`
|
Component byte `json:"component"`
|
||||||
Status string `json:"status"`
|
|
||||||
PacketNumber uint16 `json:"packet_number,omitempty"`
|
|
||||||
ErrorCode string `json:"error_code,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
type deviceEnvelope struct {
|
type deviceEnvelope struct {
|
||||||
Type, Topic, DeviceID, ReceivedAt, PayloadHex string
|
Type, Topic, DeviceID, ReceivedAt, PayloadHex string
|
||||||
@@ -49,13 +39,12 @@ type deviceEnvelope struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.Config) *Service {
|
func New(cfg config.Config) *Service {
|
||||||
return &Service{cfg: cfg, client: &http.Client{Timeout: 12 * time.Second}, byIdentity: map[string]Command{}, byIdempotency: map[string]string{}}
|
return &Service{cfg: cfg, client: &http.Client{Timeout: 12 * time.Second}}
|
||||||
}
|
}
|
||||||
func (s *Service) Run(ctx context.Context) error {
|
func (s *Service) Run(ctx context.Context) error {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
||||||
mux.HandleFunc("/v1/device-commands", s.commands)
|
mux.HandleFunc("/v1/device-commands", s.commands)
|
||||||
mux.HandleFunc("/v1/device-commands/", s.commandStatus)
|
|
||||||
mux.HandleFunc("/internal/v1/device-messages", s.deviceMessage)
|
mux.HandleFunc("/internal/v1/device-messages", s.deviceMessage)
|
||||||
s.http = &http.Server{Addr: s.cfg.HTTP.Address, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
s.http = &http.Server{Addr: s.cfg.HTTP.Address, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -95,56 +84,12 @@ func (s *Service) commands(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "command expired", 422)
|
http.Error(w, "command expired", 422)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.mu.Lock()
|
|
||||||
if existingID, ok := s.byIdempotency[cmd.IdempotencyKey]; ok {
|
|
||||||
existing := s.byIdentity[existingID]
|
|
||||||
s.mu.Unlock()
|
|
||||||
writeJSON(w, http.StatusOK, existing)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd.Status = "accepted"
|
|
||||||
cmd.CreatedAt = time.Now().UTC()
|
|
||||||
cmd.UpdatedAt = cmd.CreatedAt
|
|
||||||
s.byIdentity[cmd.Identity] = cmd
|
|
||||||
s.byIdempotency[cmd.IdempotencyKey] = cmd.Identity
|
|
||||||
s.mu.Unlock()
|
|
||||||
status, packet, err := s.dispatch(r.Context(), cmd)
|
status, packet, err := s.dispatch(r.Context(), cmd)
|
||||||
s.mu.Lock()
|
|
||||||
current := s.byIdentity[cmd.Identity]
|
|
||||||
current.UpdatedAt = time.Now().UTC()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
current.Status = "dispatch_failed"
|
writeJSON(w, http.StatusBadGateway, map[string]any{"identity": cmd.Identity, "status": "dispatch_failed", "error_code": "IOT_DISPATCH_FAILED"})
|
||||||
current.ErrorCode = "IOT_DISPATCH_FAILED"
|
|
||||||
} else {
|
|
||||||
current.Status = status
|
|
||||||
current.PacketNumber = packet
|
|
||||||
}
|
|
||||||
s.byIdentity[cmd.Identity] = current
|
|
||||||
s.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
writeJSON(w, http.StatusBadGateway, current)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusAccepted, current)
|
writeJSON(w, http.StatusAccepted, map[string]any{"identity": cmd.Identity, "status": status, "packet_number": packet})
|
||||||
}
|
|
||||||
func (s *Service) commandStatus(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !s.authorized(r) {
|
|
||||||
http.Error(w, "unauthorized", 401)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
identity := strings.TrimPrefix(r.URL.Path, "/v1/device-commands/")
|
|
||||||
s.mu.RLock()
|
|
||||||
cmd, ok := s.byIdentity[identity]
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, 200, cmd)
|
|
||||||
}
|
}
|
||||||
func (s *Service) deviceMessage(w http.ResponseWriter, r *http.Request) {
|
func (s *Service) deviceMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost || !s.authorized(r) {
|
if r.Method != http.MethodPost || !s.authorized(r) {
|
||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.apinb.com/heqiapp/platforms/backend/iot-client/internal/config"
|
"git.apinb.com/heqiapp/platforms/backend/iot-gateway/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDispatchPreservesIdempotency(t *testing.T) {
|
func TestDispatchForwardsCommandMetadata(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
calls++
|
calls++
|
||||||
@@ -28,7 +28,24 @@ func TestDispatchPreservesIdempotency(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommandsDoesNotDispatchDuplicateIdempotencyKey(t *testing.T) {
|
func TestDeviceMessageForwardsToPlatformAPI(t *testing.T) {
|
||||||
|
var received bool
|
||||||
|
platform := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
received = r.URL.Path == "/heqi/internal/v1/iot/device-messages" && r.Header.Get("X-Heqi-Iot-Token") == "token"
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}))
|
||||||
|
defer platform.Close()
|
||||||
|
srv := New(config.Config{HTTP: config.HTTP{InternalToken: "token"}, Upstream: config.Upstream{PlatformAPIURL: platform.URL, Token: "token"}})
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/internal/v1/device-messages", bytes.NewBufferString(`{"type":"device_message","receivedAt":"2026-08-03T12:00:00Z"}`))
|
||||||
|
request.Header.Set("X-Heqi-Iot-Token", "token")
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
srv.deviceMessage(response, request)
|
||||||
|
if response.Code != http.StatusAccepted || !received {
|
||||||
|
t.Fatalf("status=%d received=%v body=%s", response.Code, received, response.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandsForwardsEveryRequestWithoutLocalState(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
calls++
|
calls++
|
||||||
@@ -48,7 +65,7 @@ func TestCommandsDoesNotDispatchDuplicateIdempotencyKey(t *testing.T) {
|
|||||||
t.Fatalf("unexpected status %d: %s", response.Code, response.Body.String())
|
t.Fatalf("unexpected status %d: %s", response.Code, response.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if calls != 1 {
|
if calls != 2 {
|
||||||
t.Fatalf("duplicate command dispatched %d times", calls)
|
t.Fatalf("stateless gateway forwarded %d requests, want 2", calls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ func dispatchIoTOutbox(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
payload["identity"] = outbox.CommandIdentity
|
payload["identity"] = outbox.CommandIdentity
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
dispatch, err := http.NewRequestWithContext(ctx, http.MethodPost, config.Spec.IoTAPI.ClientBaseURL+"/v1/device-commands", bytes.NewReader(body))
|
dispatch, err := http.NewRequestWithContext(ctx, http.MethodPost, config.Spec.IoTAPI.GatewayBaseURL+"/v1/device-commands", bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func dispatchIoTOutbox(ctx context.Context) {
|
|||||||
dispatch.Header.Set("X-Heqi-Iot-Token", config.Spec.IoTAPI.Token)
|
dispatch.Header.Set("X-Heqi-Iot-Token", config.Spec.IoTAPI.Token)
|
||||||
dispatchResponse, err := client.Do(dispatch)
|
dispatchResponse, err := client.Do(dispatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
completeIoTOutbox(ctx, client, outbox.Identity, false, 0, "IOT_CLIENT_UNAVAILABLE")
|
completeIoTOutbox(ctx, client, outbox.Identity, false, 0, "IOT_GATEWAY_UNAVAILABLE")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var result struct {
|
var result struct {
|
||||||
@@ -114,7 +114,7 @@ func dispatchIoTOutbox(ctx context.Context) {
|
|||||||
success := (status == http.StatusAccepted || status == http.StatusOK) && decodeErr == nil
|
success := (status == http.StatusAccepted || status == http.StatusOK) && decodeErr == nil
|
||||||
errorCode := ""
|
errorCode := ""
|
||||||
if !success {
|
if !success {
|
||||||
errorCode = "IOT_CLIENT_REJECTED"
|
errorCode = "IOT_GATEWAY_REJECTED"
|
||||||
}
|
}
|
||||||
completeIoTOutbox(ctx, client, outbox.Identity, success, result.PacketNumber, errorCode)
|
completeIoTOutbox(ctx, client, outbox.Identity, success, result.PacketNumber, errorCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ PaymentAPI:
|
|||||||
IntervalSeconds: 60
|
IntervalSeconds: 60
|
||||||
IoTAPI:
|
IoTAPI:
|
||||||
PlatformBaseURL: http://127.0.0.1:12426
|
PlatformBaseURL: http://127.0.0.1:12426
|
||||||
ClientBaseURL: http://127.0.0.1:12429
|
GatewayBaseURL: http://127.0.0.1:12429
|
||||||
Token: change-me-iot-internal-token
|
Token: change-me-iot-internal-token
|
||||||
IntervalMilliseconds: 500
|
IntervalMilliseconds: 500
|
||||||
SecretKey: change-me-to-a-random-string
|
SecretKey: change-me-to-a-random-string
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type PaymentAPIConfig struct {
|
|||||||
}
|
}
|
||||||
type IoTAPIConfig struct {
|
type IoTAPIConfig struct {
|
||||||
PlatformBaseURL string `yaml:"PlatformBaseURL"`
|
PlatformBaseURL string `yaml:"PlatformBaseURL"`
|
||||||
ClientBaseURL string `yaml:"ClientBaseURL"`
|
GatewayBaseURL string `yaml:"GatewayBaseURL"`
|
||||||
Token string `yaml:"Token"`
|
Token string `yaml:"Token"`
|
||||||
IntervalMilliseconds int `yaml:"IntervalMilliseconds"`
|
IntervalMilliseconds int `yaml:"IntervalMilliseconds"`
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ func New(srvKey string) {
|
|||||||
if Spec.PaymentAPI.BaseURL == "" || Spec.PaymentAPI.Token == "" || Spec.PaymentAPI.IntervalSeconds <= 0 {
|
if Spec.PaymentAPI.BaseURL == "" || Spec.PaymentAPI.Token == "" || Spec.PaymentAPI.IntervalSeconds <= 0 {
|
||||||
panic("PaymentAPI configuration is required")
|
panic("PaymentAPI configuration is required")
|
||||||
}
|
}
|
||||||
if Spec.IoTAPI.PlatformBaseURL == "" || Spec.IoTAPI.ClientBaseURL == "" || Spec.IoTAPI.Token == "" || Spec.IoTAPI.IntervalMilliseconds <= 0 {
|
if Spec.IoTAPI.PlatformBaseURL == "" || Spec.IoTAPI.GatewayBaseURL == "" || Spec.IoTAPI.Token == "" || Spec.IoTAPI.IntervalMilliseconds <= 0 {
|
||||||
panic("IoTAPI configuration is required")
|
panic("IoTAPI configuration is required")
|
||||||
}
|
}
|
||||||
conf.PrintInfo(Spec.Addr)
|
conf.PrintInfo(Spec.Addr)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
| Web | `frontend/site` | 官网内容、导航、响应式、静态资源、可访问性、SEO 基础、构建与托管 Worker |
|
| Web | `frontend/site` | 官网内容、导航、响应式、静态资源、可访问性、SEO 基础、构建与托管 Worker |
|
||||||
| 后端 | `backend/api` | Gin API/BFF、JWT、权限、同步事务、状态动作、金额、幂等、审计、上传和数据库事实 |
|
| 后端 | `backend/api` | Gin API/BFF、JWT、权限、同步事务、状态动作、金额、幂等、审计、上传和数据库事实 |
|
||||||
| 后端 | `backend/worker` | 配置、启动、Mock 循环、优雅退出;未来 Outbox/Streams 的投递、重试、死信和积压 |
|
| 后端 | `backend/worker` | 配置、启动、Mock 循环、优雅退出;未来 Outbox/Streams 的投递、重试、死信和积压 |
|
||||||
| 后端 | `backend/iot-client`、`backend/iot-server` | 配置、启动、幂等命令、协议编解码、MQTT TLS、身份、遥测、回执和断线重连 |
|
| 后端 | `backend/iot-gateway`、`backend/iot-server` | 配置、启动、无状态转发、协议编解码、MQTT TLS、身份、遥测、回执和断线重连 |
|
||||||
|
|
||||||
不修改 `sample/`。真实支付、短信、地图、对象存储、电子合同、MQTT 厂商接入等,以受控沙箱或 Mock 验证;生产联调另设上线前检查点。
|
不修改 `sample/`。真实支付、短信、地图、对象存储、电子合同、MQTT 厂商接入等,以受控沙箱或 Mock 验证;生产联调另设上线前检查点。
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ npm run build
|
|||||||
```bash
|
```bash
|
||||||
cd backend/api && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
cd backend/api && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
||||||
cd backend/worker && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
cd backend/worker && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
||||||
cd backend/iot-client && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
cd backend/iot-gateway && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
||||||
cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,6 @@
|
|||||||
|
|
||||||
## 计划边界
|
## 计划边界
|
||||||
|
|
||||||
- `backend/worker` 已投递 IoT Outbox;`backend/iot-client` 与内嵌 Mochi MQTT Broker 的 `backend/iot-server` 已落地系统接口和 MQTT 协议边界。真实设备证书和厂商硬件仍需在联调环境验收。
|
- `backend/worker` 已投递 IoT Outbox;无状态 `backend/iot-gateway` 与内嵌 Mochi MQTT Broker 的 `backend/iot-server` 已落地系统接口和 MQTT 协议边界。真实设备证书和厂商硬件仍需在联调环境验收。
|
||||||
- `frontend/site` 是官网,按展示、响应式、可访问性、链接、构建和托管 Worker 测试;不把它当成业务事实写入端。
|
- `frontend/site` 是官网,按展示、响应式、可访问性、链接、构建和托管 Worker 测试;不把它当成业务事实写入端。
|
||||||
- 本计划不把尚未实现或文档中“待确认”的政策当作通过标准。此类项进入阻塞/待决清单,由产品、安全、财务或法务确认后再固化用例。
|
- 本计划不把尚未实现或文档中“待确认”的政策当作通过标准。此类项进入阻塞/待决清单,由产品、安全、财务或法务确认后再固化用例。
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ flowchart LR
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `api` | 用户端和五个管理系统的 HTTP API、鉴权、同步业务事务 | 无状态部署;所有写操作支持幂等键、事务和审计 |
|
| `api` | 用户端和五个管理系统的 HTTP API、鉴权、同步业务事务 | 无状态部署;所有写操作支持幂等键、事务和审计 |
|
||||||
| `worker` | 事件消费、告警、派单、推送、对账、超时扫描、轨迹异常识别 | Redis Streams 消费组;重试、死信、幂等消费和可观测的积压告警 |
|
| `worker` | 事件消费、告警、派单、推送、对账、超时扫描、轨迹异常识别 | Redis Streams 消费组;重试、死信、幂等消费和可观测的积压告警 |
|
||||||
| `iot-client` | 管理系统/App 的设备上下行 HTTP 边界、幂等受理与状态查询 | 不绕过 API 的 JWT、角色、对象归属与安全状态校验 |
|
| `iot-gateway` | API/Worker 与 IoT Server 的无状态上下行 HTTP 轻网关 | 不保存业务事实;不绕过 API 的 JWT、角色、对象归属与安全状态校验 |
|
||||||
| `iot-server` | 内嵌 Mochi MQTT Broker、厂商协议适配、遥测校验、命令下发与回执 | 保持设备会话一致性;每设备认证与 Topic ACL;协议版本、设备身份、LRC8 与 AES 校验 |
|
| `iot-server` | 内嵌 Mochi MQTT Broker、厂商协议适配、遥测校验、命令下发与回执 | 保持设备会话一致性;每设备认证与 Topic ACL;协议版本、设备身份、LRC8 与 AES 校验 |
|
||||||
|
|
||||||
关键业务采用“数据库事务 + Outbox 事件表 + Worker 投递”的模式:先在 PostgreSQL 提交业务事实与待投递事件,再异步写入 Redis Streams。这样 Redis 故障或 Worker 重启不会丢失订单、告警、支付或设备命令的业务事实。
|
关键业务采用“数据库事务 + Outbox 事件表 + Worker 投递”的模式:先在 PostgreSQL 提交业务事实与待投递事件,再异步写入 Redis Streams。这样 Redis 故障或 Worker 重启不会丢失订单、告警、支付或设备命令的业务事实。
|
||||||
@@ -63,7 +63,7 @@ flowchart LR
|
|||||||
| 基线 | 路径 | 使用要求 |
|
| 基线 | 路径 | 使用要求 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 前端工程基线 | 现有 Vue 管理端 | Vue 管理系统统一前端框架、路由、状态管理、请求封装、权限指令、表格表单、主题、错误处理、国际化与测试规范 |
|
| 前端工程基线 | 现有 Vue 管理端 | Vue 管理系统统一前端框架、路由、状态管理、请求封装、权限指令、表格表单、主题、错误处理、国际化与测试规范 |
|
||||||
| 后端工程基线 | `backend/{api,worker,iot-client,iot-server}` | Go API、Worker 与两个 IoT 进程统一沿用配置、日志、错误码、认证、任务、测试和发布规范 |
|
| 后端工程基线 | `backend/{api,worker,iot-gateway,iot-server}` | Go API、Worker 与两个 IoT 进程统一沿用配置、日志、错误码、认证、任务、测试和发布规范 |
|
||||||
|
|
||||||
业务项目应通过共享包、模板或上游同步机制复用标准库,禁止将标准库目录复制到每个子项目后自行漂移。标准库升级需要记录版本、影响范围、兼容策略和回滚方式。
|
业务项目应通过共享包、模板或上游同步机制复用标准库,禁止将标准库目录复制到每个子项目后自行漂移。标准库升级需要记录版本、影响范围、兼容策略和回滚方式。
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ platforms/
|
|||||||
backend/
|
backend/
|
||||||
api/ # Go HTTP API、BFF、同步领域事务
|
api/ # Go HTTP API、BFF、同步领域事务
|
||||||
worker/ # Go 异步任务:派单、告警、通知、对账、超时扫描
|
worker/ # Go 异步任务:派单、告警、通知、对账、超时扫描
|
||||||
iot-client/ # 管理系统/App 的设备上下行接口
|
iot-gateway/ # API/Worker 与 IoT Server 的无状态上下行网关
|
||||||
iot-server/ # MQTT 会话、厂商协议、设备命令、遥测与回执
|
iot-server/ # MQTT 会话、厂商协议、设备命令、遥测与回执
|
||||||
contracts/
|
contracts/
|
||||||
openapi/ # HTTP API 契约及生成配置
|
openapi/ # HTTP API 契约及生成配置
|
||||||
@@ -114,7 +114,7 @@ platforms/
|
|||||||
performance/ # 遥测、订单、轨迹与消息积压压测
|
performance/ # 遥测、订单、轨迹与消息积压压测
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 `apps/user_app`、`apps/service_app`、`backend/{api,worker,iot-client,iot-server}` 与当前管理端目录已经落地;其余标记为规划的目录仍不得因局部任务提前创建空壳。
|
其中 `apps/user_app`、`apps/service_app`、`backend/{api,worker,iot-gateway,iot-server}` 与当前管理端目录已经落地;其余标记为规划的目录仍不得因局部任务提前创建空壳。
|
||||||
|
|
||||||
## 6. 后端领域划分
|
## 6. 后端领域划分
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
## 3. IoT 协议与可靠性
|
## 3. IoT 协议与可靠性
|
||||||
|
|
||||||
- 设备厂商 V1.8 二进制帧保持 `0x5E` 起始、`0x5B` 结束、大端序、数据包 AES-128 与 LRC8 规则不变,并作为 MQTT payload 传输;Topic 使用 `devices/{deviceId}/{up|down|ack}`,QoS 1,下行命令禁止 retained。
|
- 设备厂商 V1.8 二进制帧保持 `0x5E` 起始、`0x5B` 结束、大端序、数据包 AES-128 与 LRC8 规则不变,并作为 MQTT payload 传输;Topic 使用 `devices/{deviceId}/{up|down|ack}`,QoS 1,下行命令禁止 retained。
|
||||||
- `iot-server` 使用 Mochi MQTT v2 内嵌 Broker,只处理 MQTT 会话、每设备认证/Topic ACL 和设备协议;`iot-client` 只提供系统侧上下行接口;命令、Outbox、原始上行和回执事实由 API 持久化,Worker 负责可重试投递。
|
- `iot-server` 使用 Mochi MQTT v2 内嵌 Broker,只处理 MQTT 会话、每设备认证/Topic ACL 和设备协议;`iot-gateway` 是不保存业务事实的无状态上下行轻网关;命令、幂等、Outbox、原始上行和回执事实由 API 持久化,Worker 负责可重试投递。
|
||||||
- 协议封面版本与变更记录冲突时以最新 V1.8 变更记录和绿色标注为兼容实现依据;重复子标识等歧义必须保留原始报文并按设备型号配置解析,不得静默猜测。
|
- 协议封面版本与变更记录冲突时以最新 V1.8 变更记录和绿色标注为兼容实现依据;重复子标识等歧义必须保留原始报文并按设备型号配置解析,不得静默猜测。
|
||||||
|
|
||||||
- 设备采用 MQTT over TLS,设备身份使用每设备证书或短期轮换令牌;禁止共享默认密钥。
|
- 设备采用 MQTT over TLS,设备身份使用每设备证书或短期轮换令牌;禁止共享默认密钥。
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
mkdir -p ./output
|
mkdir -p ./output
|
||||||
for module in api worker iot-client iot-server; do
|
for module in api worker iot-gateway iot-server; do
|
||||||
(cd "./backend/${module}" && GOARCH=amd64 GOOS=linux go build -o "../../output/${module}" ./cmd/main/main.go)
|
(cd "./backend/${module}" && GOARCH=amd64 GOOS=linux go build -o "../../output/${module}" ./cmd/main/main.go)
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ PIDS=()
|
|||||||
cleanup(){ trap - EXIT INT TERM; for pid in "${PIDS[@]}"; do kill "${pid}" 2>/dev/null || true; done; for pid in "${PIDS[@]}"; do wait "${pid}" 2>/dev/null || true; done; }
|
cleanup(){ trap - EXIT INT TERM; for pid in "${PIDS[@]}"; do kill "${pid}" 2>/dev/null || true; done; for pid in "${PIDS[@]}"; do wait "${pid}" 2>/dev/null || true; done; }
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
(cd "${PROJECT_ROOT}/backend/iot-server" && go run ./cmd/main/main.go -config etc/platform_iot_server_dev.yaml) & PIDS+=("$!")
|
(cd "${PROJECT_ROOT}/backend/iot-server" && go run ./cmd/main/main.go -config etc/platform_iot_server_dev.yaml) & PIDS+=("$!")
|
||||||
(cd "${PROJECT_ROOT}/backend/iot-client" && go run ./cmd/main/main.go -config etc/platform_iot_client_dev.yaml) & PIDS+=("$!")
|
(cd "${PROJECT_ROOT}/backend/iot-gateway" && go run ./cmd/main/main.go -config etc/platform_iot_gateway_dev.yaml) & PIDS+=("$!")
|
||||||
echo "IoT Server :12428 与 IoT Client :12429 已启动。"
|
echo "IoT Server :12428 与 IoT Gateway :12429 已启动。"
|
||||||
wait -n "${PIDS[@]}"
|
wait -n "${PIDS[@]}"
|
||||||
|
|||||||
Reference in New Issue
Block a user