issue_36 #47
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
Always open `@/openspec/AGENTS.md` when the request:
|
||||||
|
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||||
|
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||||
|
- Sounds ambiguous and you need the authoritative spec before coding
|
||||||
|
|
||||||
|
Use `@/openspec/AGENTS.md` to learn:
|
||||||
|
- How to create and apply change proposals
|
||||||
|
- Spec format and conventions
|
||||||
|
- Project structure and guidelines
|
||||||
|
|
||||||
|
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
@@ -7,7 +7,7 @@ app:
|
|||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
port: 8080 # 服务器监听端口
|
port: 8080 # 服务器监听端口
|
||||||
mode: "debug" # 运行模式: debug, release, test
|
mode: "debug" # 服务运行模式: debug, release, test
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
log:
|
log:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ app:
|
|||||||
# HTTP 服务配置
|
# HTTP 服务配置
|
||||||
server:
|
server:
|
||||||
port: 8086
|
port: 8086
|
||||||
mode: "release" # Gin 运行模式: "debug", "release", "test"
|
mode: "release" # 服务运行模式: "debug", "release", "test"
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
log:
|
log:
|
||||||
|
|||||||
55
go.mod
55
go.mod
@@ -3,23 +3,21 @@ module git.huangwc.com/pig/pig-farm-controller
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.1
|
|
||||||
github.com/go-openapi/errors v0.22.2
|
github.com/go-openapi/errors v0.22.2
|
||||||
github.com/go-openapi/runtime v0.28.0
|
github.com/go-openapi/runtime v0.28.0
|
||||||
github.com/go-openapi/strfmt v0.23.0
|
github.com/go-openapi/strfmt v0.23.0
|
||||||
github.com/go-openapi/swag v0.24.1
|
github.com/go-openapi/swag v0.25.1
|
||||||
github.com/go-openapi/validate v0.24.0
|
github.com/go-openapi/validate v0.24.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/panjf2000/ants/v2 v2.11.3
|
github.com/panjf2000/ants/v2 v2.11.3
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/swaggo/files v1.0.1
|
|
||||||
github.com/swaggo/gin-swagger v1.6.1
|
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.43.0
|
||||||
google.golang.org/protobuf v1.36.9
|
google.golang.org/protobuf v1.36.9
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
@@ -39,25 +37,26 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.0 // indirect
|
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.1 // indirect
|
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||||
github.com/go-openapi/loads v0.22.0 // indirect
|
github.com/go-openapi/loads v0.22.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.22.0 // indirect
|
||||||
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
|
github.com/go-openapi/swag/cmdutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/conv v0.24.0 // indirect
|
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
|
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
|
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
|
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/loading v0.24.0 // indirect
|
github.com/go-openapi/swag/loading v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/mangling v0.24.0 // indirect
|
github.com/go-openapi/swag/mangling v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/netutils v0.24.0 // indirect
|
github.com/go-openapi/swag/netutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
|
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
|
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||||
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
|
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
@@ -72,8 +71,10 @@ require (
|
|||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.1 // indirect
|
github.com/mailru/easyjson v0.9.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -85,20 +86,26 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1 // indirect
|
||||||
|
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.21.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/time v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gorm.io/driver/mysql v1.5.6 // indirect
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
64
go.sum
64
go.sum
@@ -17,6 +17,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
@@ -34,40 +36,70 @@ github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrY
|
|||||||
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
|
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
|
||||||
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||||
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
|
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
|
||||||
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
|
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||||
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
|
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
|
||||||
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
|
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
|
||||||
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
|
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
|
||||||
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
|
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
|
||||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||||
|
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||||
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
|
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
|
||||||
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
|
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
|
||||||
|
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
|
||||||
|
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
|
||||||
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
|
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
|
||||||
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
|
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
|
||||||
|
github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY=
|
||||||
|
github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||||
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
|
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
|
||||||
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
|
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||||
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
|
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
|
||||||
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
|
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
|
||||||
|
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
|
||||||
|
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
|
||||||
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
||||||
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||||
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
|
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
|
||||||
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
|
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||||
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
|
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
|
||||||
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
|
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||||
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
|
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
|
||||||
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
|
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
|
||||||
|
github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
|
||||||
|
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
|
||||||
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
|
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
|
||||||
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
|
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
|
||||||
|
github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4=
|
||||||
|
github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE=
|
||||||
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
|
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
|
||||||
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
|
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||||
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
|
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
|
||||||
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
|
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||||
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
|
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
|
||||||
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
|
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@@ -116,10 +148,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||||
|
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
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=
|
||||||
@@ -159,8 +197,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
|
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||||
|
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||||
|
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
@@ -171,6 +215,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||||
@@ -188,21 +236,29 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
|||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
@@ -216,6 +272,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -225,11 +283,17 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// API 结构体定义了 HTTP 服务器及其依赖
|
// API 结构体定义了 HTTP 服务器及其依赖
|
||||||
type API struct {
|
type API struct {
|
||||||
engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求
|
echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求
|
||||||
logger *logs.Logger // 日志记录器,用于输出日志信息
|
logger *logs.Logger // 日志记录器,用于输出日志信息
|
||||||
userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作
|
userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作
|
||||||
tokenService token.Service // Token 服务接口,用于 JWT token 的生成和解析
|
tokenService token.Service // Token 服务接口,用于 JWT token 的生成和解析
|
||||||
@@ -58,7 +59,7 @@ type API struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAPI 创建并返回一个新的 API 实例
|
// NewAPI 创建并返回一个新的 API 实例
|
||||||
// 负责初始化 Gin 引擎、设置全局中间件,并注入所有必要的依赖。
|
// 负责初始化 Echo 引擎、设置全局中间件,并注入所有必要的依赖。
|
||||||
func NewAPI(cfg config.ServerConfig,
|
func NewAPI(cfg config.ServerConfig,
|
||||||
logger *logs.Logger,
|
logger *logs.Logger,
|
||||||
userRepo repository.UserRepository,
|
userRepo repository.UserRepository,
|
||||||
@@ -75,21 +76,19 @@ func NewAPI(cfg config.ServerConfig,
|
|||||||
deviceService domain_device.Service,
|
deviceService domain_device.Service,
|
||||||
listenHandler webhook.ListenHandler,
|
listenHandler webhook.ListenHandler,
|
||||||
analysisTaskManager *scheduler.AnalysisPlanTaskManager) *API {
|
analysisTaskManager *scheduler.AnalysisPlanTaskManager) *API {
|
||||||
// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式)
|
// 使用 echo.New() 创建一个 Echo 引擎实例
|
||||||
// 从配置中获取 Gin 模式
|
e := echo.New()
|
||||||
gin.SetMode(cfg.Mode)
|
|
||||||
|
|
||||||
// 使用 gin.New() 创建一个 Gin 引擎实例,而不是 gin.Default()
|
// 根据配置设置 Echo 的调试模式
|
||||||
// 这样可以手动添加所需的中间件,避免 gin.Default() 默认包含的 Logger 和 Recovery 中间件
|
e.Debug = cfg.Mode == "debug"
|
||||||
engine := gin.New()
|
|
||||||
|
|
||||||
// 添加 Gin Recovery 中间件,用于捕获 panic 并恢复,防止服务崩溃
|
// 添加 Echo Recovery 中间件,用于捕获 panic 并恢复,防止服务崩溃
|
||||||
// gin.Logger() 已移除,因为我们使用自定义的 logger
|
// Echo 的 Logger 中间件默认会记录请求信息,如果需要自定义,可以替换
|
||||||
engine.Use(gin.Recovery())
|
e.Use(middleware.Recover())
|
||||||
|
|
||||||
// 初始化 API 结构体
|
// 初始化 API 结构体
|
||||||
api := &API{
|
api := &API{
|
||||||
engine: engine,
|
echo: e,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
tokenService: tokenService,
|
tokenService: tokenService,
|
||||||
@@ -124,7 +123,7 @@ func (a *API) Start() {
|
|||||||
// 初始化标准库的 http.Server 实例
|
// 初始化标准库的 http.Server 实例
|
||||||
a.httpServer = &http.Server{
|
a.httpServer = &http.Server{
|
||||||
Addr: addr, // 服务器监听的地址从配置中获取
|
Addr: addr, // 服务器监听的地址从配置中获取
|
||||||
Handler: a.engine, // 将 Gin 引擎作为 HTTP 请求的处理程序
|
Handler: a.echo, // 将 Echo 引擎作为 HTTP 请求的处理程序
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在独立的 goroutine 中启动服务器
|
// 在独立的 goroutine 中启动服务器
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
echoSwagger "github.com/swaggo/echo-swagger"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupRoutes 设置所有 API 路由
|
// setupRoutes 设置所有 API 路由
|
||||||
// 在此方法中,使用已初始化的控制器实例将其路由注册到 Gin 引擎中。
|
// 在此方法中,使用已初始化的控制器实例将其路由注册到 Echo 引擎中。
|
||||||
func (a *API) setupRoutes() {
|
func (a *API) setupRoutes() {
|
||||||
a.logger.Info("开始初始化所有 API 路由")
|
a.logger.Info("开始初始化所有 API 路由")
|
||||||
|
|
||||||
@@ -18,39 +18,39 @@ func (a *API) setupRoutes() {
|
|||||||
// 这些路由不需要身份验证
|
// 这些路由不需要身份验证
|
||||||
|
|
||||||
// 用户注册和登录
|
// 用户注册和登录
|
||||||
a.engine.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
|
a.echo.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
|
||||||
a.engine.POST("/api/v1/users/login", a.userController.Login) // 用户登录
|
a.echo.POST("/api/v1/users/login", a.userController.Login) // 用户登录
|
||||||
a.logger.Debug("公开接口注册成功:用户注册、登录")
|
a.logger.Debug("公开接口注册成功:用户注册、登录")
|
||||||
|
|
||||||
// 注册 pprof 路由
|
// 注册 pprof 路由
|
||||||
pprofGroup := a.engine.Group("/debug/pprof")
|
pprofGroup := a.echo.Group("/debug/pprof")
|
||||||
{
|
{
|
||||||
pprofGroup.GET("/", gin.WrapF(pprof.Index)) // pprof 索引页
|
pprofGroup.GET("/", echo.WrapHandler(http.HandlerFunc(pprof.Index))) // pprof 索引页
|
||||||
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) // pprof 命令行参数
|
pprofGroup.GET("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline))) // pprof 命令行参数
|
||||||
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) // pprof CPU profile
|
pprofGroup.GET("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile))) // pprof CPU profile
|
||||||
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (POST)
|
pprofGroup.POST("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) // pprof 符号查找 (POST)
|
||||||
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (GET)
|
pprofGroup.GET("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) // pprof 符号查找 (GET)
|
||||||
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) // pprof 跟踪
|
pprofGroup.GET("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace))) // pprof 跟踪
|
||||||
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) // pprof 内存分配
|
pprofGroup.GET("/allocs", echo.WrapHandler(pprof.Handler("allocs"))) // pprof 内存分配
|
||||||
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block"))) // pprof 阻塞
|
pprofGroup.GET("/block", echo.WrapHandler(pprof.Handler("block"))) // pprof 阻塞
|
||||||
pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
|
pprofGroup.GET("/goroutine", echo.WrapHandler(pprof.Handler("goroutine")))
|
||||||
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap"))) // pprof 堆内存
|
pprofGroup.GET("/heap", echo.WrapHandler(pprof.Handler("heap"))) // pprof 堆内存
|
||||||
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) // pprof 互斥锁
|
pprofGroup.GET("/mutex", echo.WrapHandler(pprof.Handler("mutex"))) // pprof 互斥锁
|
||||||
pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
|
pprofGroup.GET("/threadcreate", echo.WrapHandler(pprof.Handler("threadcreate")))
|
||||||
}
|
}
|
||||||
a.logger.Debug("pprof 接口注册成功")
|
a.logger.Debug("pprof 接口注册成功")
|
||||||
|
|
||||||
// 上行事件监听路由
|
// 上行事件监听路由
|
||||||
a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) // 处理设备上行事件
|
a.echo.POST("/upstream", echo.WrapHandler(a.listenHandler.Handler())) // 处理设备上行事件
|
||||||
a.logger.Debug("上行事件监听接口注册成功")
|
a.logger.Debug("上行事件监听接口注册成功")
|
||||||
|
|
||||||
// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
|
// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
|
||||||
a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Swagger UI 接口
|
a.echo.GET("/swagger/*any", echoSwagger.WrapHandler) // Swagger UI 接口
|
||||||
a.logger.Debug("Swagger UI 接口注册成功")
|
a.logger.Debug("Swagger UI 接口注册成功")
|
||||||
|
|
||||||
// --- Authenticated Routes ---
|
// --- Authenticated Routes ---
|
||||||
// 所有在此注册的路由都需要通过 JWT 身份验证
|
// 所有在此注册的路由都需要通过 JWT 身份验证
|
||||||
authGroup := a.engine.Group("/api/v1")
|
authGroup := a.echo.Group("/api/v1")
|
||||||
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
|
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
|
||||||
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件
|
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrUserNotFoundInContext 表示在 gin.Context 中未找到用户信息。
|
// ErrUserNotFoundInContext 表示在 context 中未找到用户信息。
|
||||||
ErrUserNotFoundInContext = errors.New("context中未找到用户信息")
|
ErrUserNotFoundInContext = errors.New("context中未找到用户信息")
|
||||||
// ErrInvalidUserType 表示从 gin.Context 中获取的用户信息类型不正确。
|
// ErrInvalidUserType 表示从 context 中获取的用户信息类型不正确。
|
||||||
ErrInvalidUserType = errors.New("context中用户信息类型不正确")
|
ErrInvalidUserType = errors.New("context中用户信息类型不正确")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetOperatorIDFromContext 从 gin.Context 中提取操作者ID。
|
// GetOperatorIDFromContext 从 echo.Context 中提取操作者ID。
|
||||||
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
|
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
|
||||||
func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
|
func GetOperatorIDFromContext(c echo.Context) (uint, error) {
|
||||||
userVal, exists := c.Get(models.ContextUserKey.String())
|
userVal := c.Get(models.ContextUserKey.String())
|
||||||
if !exists {
|
if userVal == nil {
|
||||||
return 0, ErrUserNotFoundInContext
|
return 0, ErrUserNotFoundInContext
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,11 +30,11 @@ func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
|
|||||||
return user.ID, nil
|
return user.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOperatorFromContext 从 gin.Context 中提取操作者。
|
// GetOperatorFromContext 从 echo.Context 中提取操作者。
|
||||||
// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 字段。
|
// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的字段。
|
||||||
func GetOperatorFromContext(c *gin.Context) (*models.User, error) {
|
func GetOperatorFromContext(c echo.Context) (*models.User, error) {
|
||||||
userVal, exists := c.Get(models.ContextUserKey.String())
|
userVal := c.Get(models.ContextUserKey.String())
|
||||||
if !exists {
|
if userVal == nil {
|
||||||
return nil, ErrUserNotFoundInContext
|
return nil, ErrUserNotFoundInContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,20 +54,18 @@ func NewController(
|
|||||||
// @Param device body dto.CreateDeviceRequest true "设备信息"
|
// @Param device body dto.CreateDeviceRequest true "设备信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||||
// @Router /api/v1/devices [post]
|
// @Router /api/v1/devices [post]
|
||||||
func (c *Controller) CreateDevice(ctx *gin.Context) {
|
func (c *Controller) CreateDevice(ctx echo.Context) error {
|
||||||
const actionType = "创建设备"
|
const actionType = "创建设备"
|
||||||
var req dto.CreateDeviceRequest
|
var req dto.CreateDeviceRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
propertiesJSON, err := json.Marshal(req.Properties)
|
propertiesJSON, err := json.Marshal(req.Properties)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device := &models.Device{
|
device := &models.Device{
|
||||||
@@ -80,32 +78,28 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := device.SelfCheck(); err != nil {
|
if err := device.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", device)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", device)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.deviceRepo.Create(device); err != nil {
|
if err := c.deviceRepo.Create(device); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "数据库创建失败", device)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "数据库创建失败", device)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createdDevice, err := c.deviceRepo.FindByID(device.ID)
|
createdDevice, err := c.deviceRepo.FindByID(device.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 重新加载创建的设备失败: %v", actionType, err)
|
c.logger.Errorf("%s: 重新加载创建的设备失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但重新加载设备失败", actionType, "重新加载设备失败", device)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但重新加载设备失败", actionType, "重新加载设备失败", device)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceResponse(createdDevice)
|
resp, err := dto.NewDeviceResponse(createdDevice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, device.ID)
|
c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, device.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDevice godoc
|
// GetDevice godoc
|
||||||
@@ -117,42 +111,37 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
|
|||||||
// @Param id path string true "设备ID"
|
// @Param id path string true "设备ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||||
// @Router /api/v1/devices/{id} [get]
|
// @Router /api/v1/devices/{id} [get]
|
||||||
func (c *Controller) GetDevice(ctx *gin.Context) {
|
func (c *Controller) GetDevice(ctx echo.Context) error {
|
||||||
const actionType = "获取设备"
|
const actionType = "获取设备"
|
||||||
deviceID := ctx.Param("id")
|
deviceID := ctx.Param("id")
|
||||||
|
|
||||||
if deviceID == "" {
|
if deviceID == "" {
|
||||||
c.logger.Errorf("%s: 设备ID为空", actionType)
|
c.logger.Errorf("%s: 设备ID为空", actionType)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备ID不能为空", actionType, "设备ID为空", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备ID不能为空", actionType, "设备ID为空", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := c.deviceRepo.FindByIDString(deviceID)
|
device, err := c.deviceRepo.FindByIDString(deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceResponse(device)
|
resp, err := dto.NewDeviceResponse(device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, device.ID)
|
c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, device.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDevices godoc
|
// ListDevices godoc
|
||||||
@@ -163,24 +152,22 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.DeviceResponse}
|
// @Success 200 {object} controller.Response{data=[]dto.DeviceResponse}
|
||||||
// @Router /api/v1/devices [get]
|
// @Router /api/v1/devices [get]
|
||||||
func (c *Controller) ListDevices(ctx *gin.Context) {
|
func (c *Controller) ListDevices(ctx echo.Context) error {
|
||||||
const actionType = "获取设备列表"
|
const actionType = "获取设备列表"
|
||||||
devices, err := c.deviceRepo.ListAll()
|
devices, err := c.deviceRepo.ListAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewListDeviceResponse(devices)
|
resp, err := dto.NewListDeviceResponse(devices)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(devices))
|
c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(devices))
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDevice godoc
|
// UpdateDevice godoc
|
||||||
@@ -194,7 +181,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
|
|||||||
// @Param device body dto.UpdateDeviceRequest true "要更新的设备信息"
|
// @Param device body dto.UpdateDeviceRequest true "要更新的设备信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||||
// @Router /api/v1/devices/{id} [put]
|
// @Router /api/v1/devices/{id} [put]
|
||||||
func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
func (c *Controller) UpdateDevice(ctx echo.Context) error {
|
||||||
const actionType = "更新设备"
|
const actionType = "更新设备"
|
||||||
deviceID := ctx.Param("id")
|
deviceID := ctx.Param("id")
|
||||||
|
|
||||||
@@ -202,31 +189,26 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdateDeviceRequest
|
var req dto.UpdateDeviceRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
propertiesJSON, err := json.Marshal(req.Properties)
|
propertiesJSON, err := json.Marshal(req.Properties)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingDevice.Name = req.Name
|
existingDevice.Name = req.Name
|
||||||
@@ -237,32 +219,28 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := existingDevice.SelfCheck(); err != nil {
|
if err := existingDevice.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", existingDevice)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", existingDevice)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.deviceRepo.Update(existingDevice); err != nil {
|
if err := c.deviceRepo.Update(existingDevice); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库更新失败: %v, Device: %+v", actionType, err, existingDevice)
|
c.logger.Errorf("%s: 数据库更新失败: %v, Device: %+v", actionType, err, existingDevice)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库更新失败", existingDevice)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库更新失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedDevice, err := c.deviceRepo.FindByID(existingDevice.ID)
|
updatedDevice, err := c.deviceRepo.FindByID(existingDevice.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 重新加载更新的设备失败: %v", actionType, err)
|
c.logger.Errorf("%s: 重新加载更新的设备失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但重新加载设备失败", actionType, "重新加载设备失败", existingDevice)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但重新加载设备失败", actionType, "重新加载设备失败", existingDevice)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceResponse(updatedDevice)
|
resp, err := dto.NewDeviceResponse(updatedDevice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, existingDevice.ID)
|
c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, existingDevice.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDevice godoc
|
// DeleteDevice godoc
|
||||||
@@ -274,37 +252,33 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
|||||||
// @Param id path string true "设备ID"
|
// @Param id path string true "设备ID"
|
||||||
// @Success 200 {object} controller.Response
|
// @Success 200 {object} controller.Response
|
||||||
// @Router /api/v1/devices/{id} [delete]
|
// @Router /api/v1/devices/{id} [delete]
|
||||||
func (c *Controller) DeleteDevice(ctx *gin.Context) {
|
func (c *Controller) DeleteDevice(ctx echo.Context) error {
|
||||||
const actionType = "删除设备"
|
const actionType = "删除设备"
|
||||||
deviceID := ctx.Param("id")
|
deviceID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(deviceID, 10, 64)
|
idUint, err := strconv.ParseUint(deviceID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备ID格式", actionType, "设备ID格式错误", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备ID格式", actionType, "设备ID格式错误", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.deviceRepo.FindByIDString(deviceID)
|
_, err = c.deviceRepo.FindByIDString(deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 查找设备失败: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 查找设备失败: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: 查找设备时发生内部错误", actionType, "数据库查询失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: 查找设备时发生内部错误", actionType, "数据库查询失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.deviceRepo.Delete(uint(idUint)); err != nil {
|
if err := c.deviceRepo.Delete(uint(idUint)); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "数据库删除失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "数据库删除失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备删除成功, ID: %d", actionType, idUint)
|
c.logger.Infof("%s: 设备删除成功, ID: %d", actionType, idUint)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManualControl godoc
|
// ManualControl godoc
|
||||||
@@ -318,32 +292,28 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) {
|
|||||||
// @Param manualControl body dto.ManualControlDeviceRequest true "手动控制指令"
|
// @Param manualControl body dto.ManualControlDeviceRequest true "手动控制指令"
|
||||||
// @Success 200 {object} controller.Response
|
// @Success 200 {object} controller.Response
|
||||||
// @Router /api/v1/devices/manual-control/{id} [post]
|
// @Router /api/v1/devices/manual-control/{id} [post]
|
||||||
func (c *Controller) ManualControl(ctx *gin.Context) {
|
func (c *Controller) ManualControl(ctx echo.Context) error {
|
||||||
const actionType = "手动控制设备"
|
const actionType = "手动控制设备"
|
||||||
deviceID := ctx.Param("id")
|
deviceID := ctx.Param("id")
|
||||||
|
|
||||||
var req dto.ManualControlDeviceRequest
|
var req dto.ManualControlDeviceRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dev, err := c.deviceRepo.FindByIDString(deviceID)
|
dev, err := c.deviceRepo.FindByIDString(deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 接收到指令, 设备ID: %s, 动作: %s", actionType, deviceID, req.Action)
|
c.logger.Infof("%s: 接收到指令, 设备ID: %s, 动作: %s", actionType, deviceID, req.Action)
|
||||||
@@ -351,7 +321,7 @@ func (c *Controller) ManualControl(ctx *gin.Context) {
|
|||||||
err = c.deviceService.Collect(dev.AreaControllerID, []*models.Device{dev})
|
err = c.deviceService.Collect(dev.AreaControllerID, []*models.Device{dev})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 获取设备状态失败: %v, 设备ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 获取设备状态失败: %v, 设备ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备状态失败: "+err.Error(), actionType, "获取设备状态失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备状态失败: "+err.Error(), actionType, "获取设备状态失败", deviceID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
action := device.DeviceActionStart
|
action := device.DeviceActionStart
|
||||||
@@ -361,17 +331,16 @@ func (c *Controller) ManualControl(ctx *gin.Context) {
|
|||||||
case "on":
|
case "on":
|
||||||
default:
|
default:
|
||||||
c.logger.Errorf("%s: 无效的动作: %s, 设备ID: %s", actionType, *req.Action, deviceID)
|
c.logger.Errorf("%s: 无效的动作: %s, 设备ID: %s", actionType, *req.Action, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的动作: "+*req.Action, actionType, "无效的动作", req.Action)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的动作: "+*req.Action, actionType, "无效的动作", req.Action)
|
||||||
}
|
}
|
||||||
err = c.deviceService.Switch(dev, action)
|
err = c.deviceService.Switch(dev, action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 设备控制失败: %v, 设备ID: %s", actionType, err, deviceID)
|
c.logger.Errorf("%s: 设备控制失败: %v, 设备ID: %s", actionType, err, deviceID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备控制失败: "+err.Error(), actionType, "设备控制失败", deviceID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备控制失败: "+err.Error(), actionType, "设备控制失败", deviceID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", map[string]interface{}{"device_id": deviceID}, actionType, "指令发送成功", gin.H{"device_id": deviceID, "action": req.Action})
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", map[string]interface{}{"device_id": deviceID}, actionType, "指令发送成功", map[string]interface{}{"device_id": deviceID, "action": req.Action})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Controller Methods: Area Controllers ---
|
// --- Controller Methods: Area Controllers ---
|
||||||
@@ -386,20 +355,18 @@ func (c *Controller) ManualControl(ctx *gin.Context) {
|
|||||||
// @Param areaController body dto.CreateAreaControllerRequest true "区域主控信息"
|
// @Param areaController body dto.CreateAreaControllerRequest true "区域主控信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||||
// @Router /api/v1/area-controllers [post]
|
// @Router /api/v1/area-controllers [post]
|
||||||
func (c *Controller) CreateAreaController(ctx *gin.Context) {
|
func (c *Controller) CreateAreaController(ctx echo.Context) error {
|
||||||
const actionType = "创建区域主控"
|
const actionType = "创建区域主控"
|
||||||
var req dto.CreateAreaControllerRequest
|
var req dto.CreateAreaControllerRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
propertiesJSON, err := json.Marshal(req.Properties)
|
propertiesJSON, err := json.Marshal(req.Properties)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ac := &models.AreaController{
|
ac := &models.AreaController{
|
||||||
@@ -411,25 +378,22 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := ac.SelfCheck(); err != nil {
|
if err := ac.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", ac)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", ac)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.areaControllerRepo.Create(ac); err != nil {
|
if err := c.areaControllerRepo.Create(ac); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "数据库创建失败", ac)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "数据库创建失败", ac)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewAreaControllerResponse(ac)
|
resp, err := dto.NewAreaControllerResponse(ac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, ac.ID)
|
c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, ac.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAreaController godoc
|
// GetAreaController godoc
|
||||||
@@ -441,38 +405,34 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
|
|||||||
// @Param id path string true "区域主控ID"
|
// @Param id path string true "区域主控ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||||
// @Router /api/v1/area-controllers/{id} [get]
|
// @Router /api/v1/area-controllers/{id} [get]
|
||||||
func (c *Controller) GetAreaController(ctx *gin.Context) {
|
func (c *Controller) GetAreaController(ctx echo.Context) error {
|
||||||
const actionType = "获取区域主控"
|
const actionType = "获取区域主控"
|
||||||
acID := ctx.Param("id")
|
acID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ac, err := c.areaControllerRepo.FindByID(uint(idUint))
|
ac, err := c.areaControllerRepo.FindByID(uint(idUint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewAreaControllerResponse(ac)
|
resp, err := dto.NewAreaControllerResponse(ac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
|
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, ac.ID)
|
c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, ac.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAreaControllers godoc
|
// ListAreaControllers godoc
|
||||||
@@ -483,24 +443,22 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.AreaControllerResponse}
|
// @Success 200 {object} controller.Response{data=[]dto.AreaControllerResponse}
|
||||||
// @Router /api/v1/area-controllers [get]
|
// @Router /api/v1/area-controllers [get]
|
||||||
func (c *Controller) ListAreaControllers(ctx *gin.Context) {
|
func (c *Controller) ListAreaControllers(ctx echo.Context) error {
|
||||||
const actionType = "获取区域主控列表"
|
const actionType = "获取区域主控列表"
|
||||||
acs, err := c.areaControllerRepo.ListAll()
|
acs, err := c.areaControllerRepo.ListAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewListAreaControllerResponse(acs)
|
resp, err := dto.NewListAreaControllerResponse(acs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
|
c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(acs))
|
c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(acs))
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAreaController godoc
|
// UpdateAreaController godoc
|
||||||
@@ -514,41 +472,36 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
|
|||||||
// @Param areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息"
|
// @Param areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||||
// @Router /api/v1/area-controllers/{id} [put]
|
// @Router /api/v1/area-controllers/{id} [put]
|
||||||
func (c *Controller) UpdateAreaController(ctx *gin.Context) {
|
func (c *Controller) UpdateAreaController(ctx echo.Context) error {
|
||||||
const actionType = "更新区域主控"
|
const actionType = "更新区域主控"
|
||||||
acID := ctx.Param("id")
|
acID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingAC, err := c.areaControllerRepo.FindByID(uint(idUint))
|
existingAC, err := c.areaControllerRepo.FindByID(uint(idUint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdateAreaControllerRequest
|
var req dto.UpdateAreaControllerRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
propertiesJSON, err := json.Marshal(req.Properties)
|
propertiesJSON, err := json.Marshal(req.Properties)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingAC.Name = req.Name
|
existingAC.Name = req.Name
|
||||||
@@ -558,25 +511,22 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := existingAC.SelfCheck(); err != nil {
|
if err := existingAC.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", existingAC)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", existingAC)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.areaControllerRepo.Update(existingAC); err != nil {
|
if err := c.areaControllerRepo.Update(existingAC); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库更新失败: %v, AreaController: %+v", actionType, err, existingAC)
|
c.logger.Errorf("%s: 数据库更新失败: %v, AreaController: %+v", actionType, err, existingAC)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库更新失败", existingAC)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库更新失败", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewAreaControllerResponse(existingAC)
|
resp, err := dto.NewAreaControllerResponse(existingAC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
|
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, existingAC.ID)
|
c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, existingAC.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAreaController godoc
|
// DeleteAreaController godoc
|
||||||
@@ -588,37 +538,33 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
|
|||||||
// @Param id path string true "区域主控ID"
|
// @Param id path string true "区域主控ID"
|
||||||
// @Success 200 {object} controller.Response
|
// @Success 200 {object} controller.Response
|
||||||
// @Router /api/v1/area-controllers/{id} [delete]
|
// @Router /api/v1/area-controllers/{id} [delete]
|
||||||
func (c *Controller) DeleteAreaController(ctx *gin.Context) {
|
func (c *Controller) DeleteAreaController(ctx echo.Context) error {
|
||||||
const actionType = "删除区域主控"
|
const actionType = "删除区域主控"
|
||||||
acID := ctx.Param("id")
|
acID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.areaControllerRepo.FindByID(uint(idUint))
|
_, err = c.areaControllerRepo.FindByID(uint(idUint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 查找区域主控失败: %v, ID: %s", actionType, err, acID)
|
c.logger.Errorf("%s: 查找区域主控失败: %v, ID: %s", actionType, err, acID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: 查找时发生内部错误", actionType, "数据库查询失败", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: 查找时发生内部错误", actionType, "数据库查询失败", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.areaControllerRepo.Delete(uint(idUint)); err != nil {
|
if err := c.areaControllerRepo.Delete(uint(idUint)); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "数据库删除失败", acID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "数据库删除失败", acID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 区域主控删除成功, ID: %d", actionType, idUint)
|
c.logger.Infof("%s: 区域主控删除成功, ID: %d", actionType, idUint)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Controller Methods: Device Templates ---
|
// --- Controller Methods: Device Templates ---
|
||||||
@@ -633,27 +579,24 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) {
|
|||||||
// @Param deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息"
|
// @Param deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||||
// @Router /api/v1/device-templates [post]
|
// @Router /api/v1/device-templates [post]
|
||||||
func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
|
func (c *Controller) CreateDeviceTemplate(ctx echo.Context) error {
|
||||||
const actionType = "创建设备模板"
|
const actionType = "创建设备模板"
|
||||||
var req dto.CreateDeviceTemplateRequest
|
var req dto.CreateDeviceTemplateRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commandsJSON, err := json.Marshal(req.Commands)
|
commandsJSON, err := json.Marshal(req.Commands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valuesJSON, err := json.Marshal(req.Values)
|
valuesJSON, err := json.Marshal(req.Values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceTemplate := &models.DeviceTemplate{
|
deviceTemplate := &models.DeviceTemplate{
|
||||||
@@ -667,25 +610,22 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := deviceTemplate.SelfCheck(); err != nil {
|
if err := deviceTemplate.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", deviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", deviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.deviceTemplateRepo.Create(deviceTemplate); err != nil {
|
if err := c.deviceTemplateRepo.Create(deviceTemplate); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "数据库创建失败", deviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "数据库创建失败", deviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, deviceTemplate.ID)
|
c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, deviceTemplate.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeviceTemplate godoc
|
// GetDeviceTemplate godoc
|
||||||
@@ -697,38 +637,34 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
|
|||||||
// @Param id path string true "设备模板ID"
|
// @Param id path string true "设备模板ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||||
// @Router /api/v1/device-templates/{id} [get]
|
// @Router /api/v1/device-templates/{id} [get]
|
||||||
func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
|
func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
|
||||||
const actionType = "获取设备模板"
|
const actionType = "获取设备模板"
|
||||||
dtID := ctx.Param("id")
|
dtID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
deviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
|
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, deviceTemplate.ID)
|
c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, deviceTemplate.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDeviceTemplates godoc
|
// ListDeviceTemplates godoc
|
||||||
@@ -739,24 +675,22 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
|
// @Success 200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
|
||||||
// @Router /api/v1/device-templates [get]
|
// @Router /api/v1/device-templates [get]
|
||||||
func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
|
func (c *Controller) ListDeviceTemplates(ctx echo.Context) error {
|
||||||
const actionType = "获取设备模板列表"
|
const actionType = "获取设备模板列表"
|
||||||
deviceTemplates, err := c.deviceTemplateRepo.ListAll()
|
deviceTemplates, err := c.deviceTemplateRepo.ListAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates)
|
resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
|
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(deviceTemplates))
|
c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(deviceTemplates))
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDeviceTemplate godoc
|
// UpdateDeviceTemplate godoc
|
||||||
@@ -770,48 +704,42 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
|
|||||||
// @Param deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息"
|
// @Param deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||||
// @Router /api/v1/device-templates/{id} [put]
|
// @Router /api/v1/device-templates/{id} [put]
|
||||||
func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
|
func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
|
||||||
const actionType = "更新设备模板"
|
const actionType = "更新设备模板"
|
||||||
dtID := ctx.Param("id")
|
dtID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingDeviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
existingDeviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdateDeviceTemplateRequest
|
var req dto.UpdateDeviceTemplateRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commandsJSON, err := json.Marshal(req.Commands)
|
commandsJSON, err := json.Marshal(req.Commands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valuesJSON, err := json.Marshal(req.Values)
|
valuesJSON, err := json.Marshal(req.Values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingDeviceTemplate.Name = req.Name
|
existingDeviceTemplate.Name = req.Name
|
||||||
@@ -823,25 +751,22 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := existingDeviceTemplate.SelfCheck(); err != nil {
|
if err := existingDeviceTemplate.SelfCheck(); err != nil {
|
||||||
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", existingDeviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", existingDeviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil {
|
if err := c.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库更新失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
c.logger.Errorf("%s: 数据库更新失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库更新失败", existingDeviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库更新失败", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate)
|
resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, existingDeviceTemplate.ID)
|
c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, existingDeviceTemplate.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDeviceTemplate godoc
|
// DeleteDeviceTemplate godoc
|
||||||
@@ -853,15 +778,14 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
|
|||||||
// @Param id path string true "设备模板ID"
|
// @Param id path string true "设备模板ID"
|
||||||
// @Success 200 {object} controller.Response
|
// @Success 200 {object} controller.Response
|
||||||
// @Router /api/v1/device-templates/{id} [delete]
|
// @Router /api/v1/device-templates/{id} [delete]
|
||||||
func (c *Controller) DeleteDeviceTemplate(ctx *gin.Context) {
|
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||||
const actionType = "删除设备模板"
|
const actionType = "删除设备模板"
|
||||||
dtID := ctx.Param("id")
|
dtID := ctx.Param("id")
|
||||||
|
|
||||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在尝试删除之前,先检查设备模板是否存在
|
// 在尝试删除之前,先检查设备模板是否存在
|
||||||
@@ -869,12 +793,10 @@ func (c *Controller) DeleteDeviceTemplate(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 查找设备模板失败: %v, ID: %s", actionType, err, dtID)
|
c.logger.Errorf("%s: 查找设备模板失败: %v, ID: %s", actionType, err, dtID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: 查找时发生内部错误", actionType, "数据库查询失败", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: 查找时发生内部错误", actionType, "数据库查询失败", dtID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用仓库层的删除方法,该方法会检查模板是否被使用
|
// 调用仓库层的删除方法,该方法会检查模板是否被使用
|
||||||
@@ -882,14 +804,13 @@ func (c *Controller) DeleteDeviceTemplate(ctx *gin.Context) {
|
|||||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||||
// 如果错误信息包含“设备模板正在被设备使用,无法删除”,则返回特定的错误码
|
// 如果错误信息包含“设备模板正在被设备使用,无法删除”,则返回特定的错误码
|
||||||
if strings.Contains(err.Error(), "设备模板正在被设备使用,无法删除") {
|
if strings.Contains(err.Error(), "设备模板正在被设备使用,无法删除") {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备模板正在使用", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备模板正在使用", dtID)
|
||||||
} else {
|
} else {
|
||||||
// 其他数据库错误
|
// 其他数据库错误
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "数据库删除失败", dtID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "数据库删除失败", dtID)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Infof("%s: 设备模板删除成功, ID: %d", actionType, idUint)
|
c.logger.Infof("%s: 设备模板删除成功, ID: %d", actionType, idUint)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,741 +0,0 @@
|
|||||||
package device_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"gorm.io/datatypes"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockDeviceRepository 是 DeviceRepository 接口的模拟实现
|
|
||||||
type MockDeviceRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTx 模拟 DeviceRepository 的 CreateTx 方法
|
|
||||||
func (m *MockDeviceRepository) Create(device *models.Device) error {
|
|
||||||
args := m.Called(device)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindByID 模拟 DeviceRepository 的 FindByID 方法
|
|
||||||
func (m *MockDeviceRepository) FindByID(id uint) (*models.Device, error) {
|
|
||||||
args := m.Called(id)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).(*models.Device), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindByIDString 模拟 DeviceRepository 的 FindByIDString 方法
|
|
||||||
func (m *MockDeviceRepository) FindByIDString(id string) (*models.Device, error) {
|
|
||||||
args := m.Called(id)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).(*models.Device), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAll 模拟 DeviceRepository 的 ListAll 方法
|
|
||||||
func (m *MockDeviceRepository) ListAll() ([]*models.Device, error) {
|
|
||||||
args := m.Called()
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).([]*models.Device), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListByParentID 模拟 DeviceRepository 的 ListByParentID 方法
|
|
||||||
func (m *MockDeviceRepository) ListByParentID(parentID *uint) ([]*models.Device, error) {
|
|
||||||
args := m.Called(parentID)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).([]*models.Device), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update 模拟 DeviceRepository 的 Update 方法
|
|
||||||
func (m *MockDeviceRepository) Update(device *models.Device) error {
|
|
||||||
args := m.Called(device)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 模拟 DeviceRepository 的 Delete 方法
|
|
||||||
func (m *MockDeviceRepository) Delete(id uint) error {
|
|
||||||
args := m.Called(id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testCase 结构体定义了所有测试用例的通用参数
|
|
||||||
type testCase struct {
|
|
||||||
name string
|
|
||||||
httpMethod string // 新增字段:HTTP 方法
|
|
||||||
requestBody interface{}
|
|
||||||
paramID string // URL 中的 ID 参数
|
|
||||||
mockRepoSetup func(*MockDeviceRepository)
|
|
||||||
expectedStatus int // HTTP 状态码
|
|
||||||
expectedCode int // 业务状态码
|
|
||||||
expectedMessage string
|
|
||||||
expectedDataFunc func(interface{}) bool // 用于验证 data 字段的函数
|
|
||||||
}
|
|
||||||
|
|
||||||
// runTest 是一个辅助函数,用于执行单个测试用例
|
|
||||||
func runTest(t *testing.T, tc testCase, controllerMethod func(*gin.Context, *MockDeviceRepository)) {
|
|
||||||
// 初始化 Gin 上下文
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
ctx, _ := gin.CreateTestContext(w)
|
|
||||||
|
|
||||||
// 设置请求体和 HTTP 方法
|
|
||||||
if tc.requestBody != nil {
|
|
||||||
jsonBody, _ := json.Marshal(tc.requestBody)
|
|
||||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", io.NopCloser(bytes.NewBuffer(jsonBody)))
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
||||||
} else {
|
|
||||||
// 对于没有请求体的请求 (GET, DELETE, 或没有 body 的 POST/PUT)
|
|
||||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 URL 参数
|
|
||||||
if tc.paramID != "" {
|
|
||||||
ctx.Params = append(ctx.Params, gin.Param{Key: "id", Value: tc.paramID})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Mock Repository
|
|
||||||
mockRepo := new(MockDeviceRepository)
|
|
||||||
// 设置 Mock 行为
|
|
||||||
tc.mockRepoSetup(mockRepo)
|
|
||||||
|
|
||||||
// 调用被测试的方法,并传入 mockRepo
|
|
||||||
controllerMethod(ctx, mockRepo)
|
|
||||||
|
|
||||||
// 解析响应体
|
|
||||||
var responseBody controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// 断言 HTTP 状态码始终为 200 OK
|
|
||||||
assert.Equal(t, tc.expectedStatus, w.Code)
|
|
||||||
|
|
||||||
// 断言业务状态码和消息
|
|
||||||
assert.Equal(t, tc.expectedCode, responseBody.Code)
|
|
||||||
assert.Equal(t, tc.expectedMessage, responseBody.Message)
|
|
||||||
|
|
||||||
// 断言数据字段
|
|
||||||
if tc.expectedDataFunc != nil {
|
|
||||||
var data interface{}
|
|
||||||
// 只有当 responseBody.Data 不为 nil 且其底层类型为 []byte 时才尝试 Unmarshal
|
|
||||||
if responseBody.Data != nil {
|
|
||||||
if byteData, ok := responseBody.Data.([]byte); ok {
|
|
||||||
err = json.Unmarshal(byteData, &data)
|
|
||||||
assert.NoError(t, err, "无法解析响应数据") // 增加对 Unmarshal 错误的断言
|
|
||||||
} else {
|
|
||||||
// 如果 Data 不为 nil 但也不是 []byte,这通常不应该发生
|
|
||||||
// 但为了健壮性,直接将原始 interface{} 赋值给 data
|
|
||||||
data = responseBody.Data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, tc.expectedDataFunc(data), "数据字段验证失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 Mock 期望是否都已满足
|
|
||||||
mockRepo.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateDevice(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
name: "成功创建区域主控",
|
|
||||||
httpMethod: http.MethodPost,
|
|
||||||
requestBody: device.CreateDeviceRequest{
|
|
||||||
Name: "主控A",
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
Location: "猪舍1",
|
|
||||||
Properties: controller.Properties(`{"lora_address":"0x1234"}`),
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool {
|
|
||||||
// 检查 Name 字段
|
|
||||||
nameMatch := dev.Name == "主控A"
|
|
||||||
// 检查 Type 字段
|
|
||||||
typeMatch := dev.Type == models.DeviceTypeAreaController
|
|
||||||
// 检查 Location 字段
|
|
||||||
locationMatch := dev.Location == "猪舍1"
|
|
||||||
// 检查 Properties 字段的字节内容
|
|
||||||
expectedProperties := controller.Properties(`{"lora_address":"0x1234"}`)
|
|
||||||
propertiesMatch := bytes.Equal(dev.Properties, expectedProperties)
|
|
||||||
|
|
||||||
return nameMatch && typeMatch && locationMatch && propertiesMatch
|
|
||||||
})).Return(nil).Run(func(args mock.Arguments) {
|
|
||||||
// 模拟 GORM 自动填充 ID
|
|
||||||
arg := args.Get(0).(*models.Device)
|
|
||||||
arg.ID = 1
|
|
||||||
arg.CreatedAt = time.Now()
|
|
||||||
arg.UpdatedAt = time.Now()
|
|
||||||
}).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeCreated,
|
|
||||||
expectedMessage: "设备创建成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataMap, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dataMap["id"] != nil &&
|
|
||||||
dataMap["name"] == "主控A" &&
|
|
||||||
dataMap["type"] == string(models.DeviceTypeAreaController) &&
|
|
||||||
dataMap["properties"] != nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "成功创建普通设备",
|
|
||||||
httpMethod: http.MethodPost,
|
|
||||||
requestBody: device.CreateDeviceRequest{
|
|
||||||
Name: "温度传感器",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
SubType: models.SubTypeSensorTemp,
|
|
||||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
|
||||||
Location: "猪舍1-A区",
|
|
||||||
Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
|
||||||
arg := args.Get(0).(*models.Device)
|
|
||||||
arg.ID = 2
|
|
||||||
arg.CreatedAt = time.Now()
|
|
||||||
arg.UpdatedAt = time.Now()
|
|
||||||
}).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeCreated,
|
|
||||||
expectedMessage: "设备创建成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataMap, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dataMap["id"] != nil &&
|
|
||||||
dataMap["name"] == "温度传感器" &&
|
|
||||||
dataMap["type"] == string(models.DeviceTypeDevice) &&
|
|
||||||
dataMap["sub_type"] == string(models.SubTypeSensorTemp) &&
|
|
||||||
dataMap["parent_id"] != nil &&
|
|
||||||
dataMap["properties"] != nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败",
|
|
||||||
httpMethod: http.MethodPost,
|
|
||||||
requestBody: device.CreateDeviceRequest{
|
|
||||||
Name: "", // 缺少必填字段 Name
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeBadRequest,
|
|
||||||
expectedMessage: "Key: 'CreateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "数据库创建失败",
|
|
||||||
httpMethod: http.MethodPost,
|
|
||||||
requestBody: device.CreateDeviceRequest{
|
|
||||||
Name: "失败设备",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError,
|
|
||||||
expectedMessage: "创建设备失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
// 新增:Properties字段JSON格式无效
|
|
||||||
{
|
|
||||||
name: "Properties字段JSON格式无效",
|
|
||||||
httpMethod: http.MethodPost,
|
|
||||||
requestBody: device.CreateDeviceRequest{
|
|
||||||
Name: "无效JSON设备",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
Properties: controller.Properties(`{invalid json}`),
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
// 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误
|
|
||||||
// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
|
|
||||||
m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
|
||||||
dev := args.Get(0).(*models.Device)
|
|
||||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
|
||||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
|
||||||
expectedProperties := controller.Properties(`{invalid json}`)
|
|
||||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
|
||||||
}).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK, // HTTP status is 200 OK for business errors
|
|
||||||
expectedCode: controller.CodeInternalError, // Business code for internal server error
|
|
||||||
expectedMessage: "创建设备失败", // The message returned by the controller
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
|
||||||
device.NewController(repo, logs.NewSilentLogger()).CreateDevice(ctx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDevice(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
name: "成功获取设备",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 1,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Name: "测试设备",
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
Location: "测试地点",
|
|
||||||
Properties: datatypes.JSON(`{"key":"value"}`),
|
|
||||||
}, nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "获取设备信息成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataMap, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dataMap["id"] == float64(1) &&
|
|
||||||
dataMap["name"] == "测试设备" &&
|
|
||||||
dataMap["properties"] != nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "设备未找到",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "999",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeNotFound,
|
|
||||||
expectedMessage: "设备未找到",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ID格式无效",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "abc",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeBadRequest,
|
|
||||||
expectedMessage: "无效的设备ID格式",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "数据库查询失败",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "1").Return(nil, errors.New("db error")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError,
|
|
||||||
expectedMessage: "获取设备信息失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
|
||||||
device.NewController(repo, logs.NewSilentLogger()).GetDevice(ctx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListDevices(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
name: "成功获取空列表",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("ListAll").Return([]*models.Device{}, nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "获取设备列表成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
s, ok := data.([]interface{})
|
|
||||||
return ok && len(s) == 0
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "成功获取包含设备的列表",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("ListAll").Return([]*models.Device{
|
|
||||||
{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 1,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Name: "设备1",
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 2,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Name: "设备2",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
SubType: models.SubTypeFan,
|
|
||||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
|
||||||
},
|
|
||||||
}, nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "获取设备列表成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataList, ok := data.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 检查长度
|
|
||||||
if len(dataList) != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 检查第一个设备
|
|
||||||
item1, ok1 := dataList[0].(map[string]interface{})
|
|
||||||
if !ok1 || item1["id"] != float64(1) || item1["name"] != "设备1" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 检查第二个设备
|
|
||||||
item2, ok2 := dataList[1].(map[string]interface{})
|
|
||||||
if !ok2 || item2["id"] != float64(2) || item2["name"] != "设备2" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "数据库查询失败",
|
|
||||||
httpMethod: http.MethodGet,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("ListAll").Return(nil, errors.New("db error")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError,
|
|
||||||
expectedMessage: "获取设备列表失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
|
||||||
device.NewController(repo, logs.NewSilentLogger()).ListDevices(ctx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateDevice(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
name: "成功更新设备",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "更新后的主控",
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
Location: "新地点",
|
|
||||||
Properties: controller.Properties(`{"lora_address":"0x5678"}`),
|
|
||||||
},
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
// 模拟 FindByIDString 找到设备
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 1,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Name: "旧主控",
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
Location: "旧地点",
|
|
||||||
Properties: datatypes.JSON(`{"lora_address":"0x1234"}`),
|
|
||||||
}, nil).Once()
|
|
||||||
// 模拟 Update 成功
|
|
||||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "设备更新成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataMap, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dataMap["id"] == float64(1) &&
|
|
||||||
dataMap["name"] == "更新后的主控" &&
|
|
||||||
dataMap["location"] == "新地点" &&
|
|
||||||
dataMap["properties"] != nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "", // 缺少必填字段 Name
|
|
||||||
Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeBadRequest,
|
|
||||||
expectedMessage: "Key: 'UpdateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "设备未找到",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
paramID: "999",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeNotFound,
|
|
||||||
expectedMessage: "设备未找到",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ID格式无效",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
paramID: "abc",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeBadRequest,
|
|
||||||
expectedMessage: "无效的设备ID格式",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "数据库更新失败",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "更新失败设备", Type: models.DeviceTypeAreaController,
|
|
||||||
},
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
|
||||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(errors.New("db error")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError,
|
|
||||||
expectedMessage: "更新设备失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
// 新增:Properties字段JSON格式无效
|
|
||||||
{
|
|
||||||
name: "Properties字段JSON格式无效",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "无效JSON设备",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
Properties: controller.Properties(`{invalid json}`),
|
|
||||||
},
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
|
||||||
// 期望 Update 方法被调用,并返回一个模拟的数据库错误
|
|
||||||
m.On("Update", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
|
||||||
dev := args.Get(0).(*models.Device)
|
|
||||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
|
||||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
|
||||||
expectedProperties := controller.Properties(`{invalid json}`)
|
|
||||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
|
||||||
}).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError, // Expected to be internal server error due to DB error
|
|
||||||
expectedMessage: "更新设备失败", // The message returned by the controller
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
// 新增:成功更新设备的ParentID
|
|
||||||
{
|
|
||||||
name: "成功更新设备的ParentID",
|
|
||||||
httpMethod: http.MethodPut,
|
|
||||||
requestBody: device.UpdateDeviceRequest{
|
|
||||||
Name: "更新ParentID设备",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
ParentID: func() *uint { id := uint(10); return &id }(),
|
|
||||||
Location: "新地点",
|
|
||||||
Properties: controller.Properties(`{"key":"value"}`),
|
|
||||||
},
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
// 模拟 FindByIDString 找到设备
|
|
||||||
m.On("FindByIDString", "1").Return(&models.Device{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 1,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Name: "旧设备",
|
|
||||||
Type: models.DeviceTypeDevice,
|
|
||||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
|
||||||
Location: "旧地点",
|
|
||||||
Properties: datatypes.JSON(`{"old_key":"old_value"}`),
|
|
||||||
}, nil).Once()
|
|
||||||
// 模拟 Update 成功,并验证 ParentID 被更新
|
|
||||||
m.On("Update", mock.MatchedBy(func(dev *models.Device) bool {
|
|
||||||
return dev.ID == 1 && *dev.ParentID == 10
|
|
||||||
})).Return(nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "设备更新成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool {
|
|
||||||
dataMap, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dataMap["id"] == float64(1) &&
|
|
||||||
dataMap["parent_id"] == float64(10) &&
|
|
||||||
dataMap["properties"] != nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
|
||||||
device.NewController(repo, logs.NewSilentLogger()).UpdateDevice(ctx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteDevice(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
name: "成功删除设备",
|
|
||||||
httpMethod: http.MethodDelete,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("Delete", uint(1)).Return(nil).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeSuccess,
|
|
||||||
expectedMessage: "设备删除成功",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ID格式无效",
|
|
||||||
httpMethod: http.MethodDelete,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "abc",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeBadRequest,
|
|
||||||
expectedMessage: "无效的设备ID格式",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "数据库删除失败",
|
|
||||||
httpMethod: http.MethodDelete,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "1",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("Delete", uint(1)).Return(errors.New("db error")).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError,
|
|
||||||
expectedMessage: "删除设备失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
// 新增:删除设备未找到
|
|
||||||
{
|
|
||||||
name: "删除设备未找到",
|
|
||||||
httpMethod: http.MethodDelete,
|
|
||||||
requestBody: nil,
|
|
||||||
paramID: "999",
|
|
||||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
|
||||||
m.On("Delete", uint(999)).Return(gorm.ErrRecordNotFound).Once()
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedCode: controller.CodeInternalError, // 当前控制器逻辑会将 ErrRecordNotFound 视为内部错误
|
|
||||||
expectedMessage: "删除设备失败",
|
|
||||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
|
||||||
device.NewController(repo, logs.NewSilentLogger()).DeleteDevice(ctx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,39 +7,39 @@ import (
|
|||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapAndSendError 统一映射服务层错误并发送响应。
|
// mapAndSendError 统一映射服务层错误并发送响应。
|
||||||
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
|
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
|
||||||
func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err error, id uint) {
|
func mapAndSendError(c *PigBatchController, ctx echo.Context, action string, err error, id uint) error {
|
||||||
if errors.Is(err, service.ErrPigBatchNotFound) ||
|
if errors.Is(err, service.ErrPigBatchNotFound) ||
|
||||||
errors.Is(err, service.ErrPenNotFound) ||
|
errors.Is(err, service.ErrPenNotFound) ||
|
||||||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
|
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||||
} else if errors.Is(err, service.ErrInvalidOperation) ||
|
} else if errors.Is(err, service.ErrInvalidOperation) ||
|
||||||
errors.Is(err, service.ErrPigBatchActive) ||
|
errors.Is(err, service.ErrPigBatchActive) ||
|
||||||
errors.Is(err, service.ErrPigBatchNotActive) ||
|
errors.Is(err, service.ErrPigBatchNotActive) ||
|
||||||
errors.Is(err, service.ErrPenOccupiedByOtherBatch) ||
|
errors.Is(err, service.ErrPenOccupiedByOtherBatch) ||
|
||||||
errors.Is(err, service.ErrPenStatusInvalidForAllocation) ||
|
errors.Is(err, service.ErrPenStatusInvalidForAllocation) ||
|
||||||
errors.Is(err, service.ErrPenNotEmpty) {
|
errors.Is(err, service.ErrPenNotEmpty) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||||
} else {
|
} else {
|
||||||
c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
|
c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, fmt.Sprintf("操作失败: %v", err), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, fmt.Sprintf("操作失败: %v", err), action, err.Error(), id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// idExtractorFunc 定义了一个函数类型,用于从gin.Context中提取主ID。
|
// idExtractorFunc 定义了一个函数类型,用于从echo.Context中提取主ID。
|
||||||
type idExtractorFunc func(ctx *gin.Context) (uint, error)
|
type idExtractorFunc func(ctx echo.Context) (uint, error)
|
||||||
|
|
||||||
// extractOperatorAndPrimaryID 封装了从gin.Context中提取操作员ID和主ID的通用逻辑。
|
// extractOperatorAndPrimaryID 封装了从echo.Context中提取操作员ID和主ID的通用逻辑。
|
||||||
// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。
|
// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。
|
||||||
//
|
//
|
||||||
// 参数:
|
// 参数:
|
||||||
//
|
//
|
||||||
// c: *PigBatchController - 控制器实例,用于访问其日志。
|
// c: *PigBatchController - 控制器实例,用于访问其日志。
|
||||||
// ctx: *gin.Context - Gin上下文。
|
// ctx: echo.Context - Echo上下文。
|
||||||
// action: string - 当前操作的描述,用于日志和审计。
|
// action: string - 当前操作的描述,用于日志和审计。
|
||||||
// idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。
|
// idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。
|
||||||
//
|
//
|
||||||
@@ -47,26 +47,24 @@ type idExtractorFunc func(ctx *gin.Context) (uint, error)
|
|||||||
//
|
//
|
||||||
// operatorID: uint - 提取到的操作员ID。
|
// operatorID: uint - 提取到的操作员ID。
|
||||||
// primaryID: uint - 提取到的主ID。
|
// primaryID: uint - 提取到的主ID。
|
||||||
// ok: bool - 如果ID提取成功且没有发送错误响应,则为true。
|
// err: error - 如果ID提取失败或发送错误响应,则返回错误。
|
||||||
func extractOperatorAndPrimaryID(
|
func extractOperatorAndPrimaryID(
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
idExtractor idExtractorFunc,
|
idExtractor idExtractorFunc,
|
||||||
) (operatorID uint, primaryID uint, ok bool) {
|
) (operatorID uint, primaryID uint, err error) {
|
||||||
// 1. 获取操作员ID
|
// 1. 获取操作员ID
|
||||||
operatorID, err := controller.GetOperatorIDFromContext(ctx)
|
operatorID, err = controller.GetOperatorIDFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||||
return 0, 0, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 提取主ID
|
// 2. 提取主ID
|
||||||
if idExtractor != nil {
|
if idExtractor != nil {
|
||||||
primaryID, err = idExtractor(ctx)
|
primaryID, err = idExtractor(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
|
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
|
||||||
return 0, 0, false
|
|
||||||
}
|
}
|
||||||
} else { // 默认从 ":id" 路径参数提取
|
} else { // 默认从 ":id" 路径参数提取
|
||||||
idParam := ctx.Param("id")
|
idParam := ctx.Param("id")
|
||||||
@@ -75,165 +73,155 @@ func extractOperatorAndPrimaryID(
|
|||||||
} else {
|
} else {
|
||||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
|
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
|
||||||
return 0, 0, false
|
|
||||||
}
|
}
|
||||||
primaryID = uint(parsedID)
|
primaryID = uint(parsedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return operatorID, primaryID, true
|
return operatorID, primaryID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
|
// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
|
||||||
// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
|
// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
|
||||||
func handleAPIRequest[Req any](
|
func handleAPIRequest[Req any](
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
reqDTO Req,
|
reqDTO Req,
|
||||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error,
|
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) error,
|
||||||
successMsg string,
|
successMsg string,
|
||||||
idExtractor idExtractorFunc,
|
idExtractor idExtractorFunc,
|
||||||
) {
|
) error {
|
||||||
// 1. 绑定请求体
|
// 1. 绑定请求体
|
||||||
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
|
if err := ctx.Bind(&reqDTO); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 提取操作员ID和主ID
|
// 2. 提取操作员ID和主ID
|
||||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 执行服务层逻辑
|
// 3. 执行服务层逻辑
|
||||||
err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
err = serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mapAndSendError(c, ctx, action, err, primaryID)
|
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送成功响应
|
// 4. 发送成功响应
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNoBodyAPIRequest 封装了处理不带请求体,但有路径参数和操作员ID的API请求的通用逻辑。
|
// handleNoBodyAPIRequest 封装了处理不带请求体,但有路径参数和操作员ID的API请求的通用逻辑。
|
||||||
func handleNoBodyAPIRequest(
|
func handleNoBodyAPIRequest(
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) error,
|
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) error,
|
||||||
successMsg string,
|
successMsg string,
|
||||||
idExtractor idExtractorFunc,
|
idExtractor idExtractorFunc,
|
||||||
) {
|
) error {
|
||||||
// 1. 提取操作员ID和主ID
|
// 1. 提取操作员ID和主ID
|
||||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 执行服务层逻辑
|
// 2. 执行服务层逻辑
|
||||||
err := serviceExecutor(ctx, operatorID, primaryID)
|
err = serviceExecutor(ctx, operatorID, primaryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mapAndSendError(c, ctx, action, err, primaryID)
|
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发送成功响应
|
// 3. 发送成功响应
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
|
// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
|
||||||
func handleAPIRequestWithResponse[Req any, Resp any](
|
func handleAPIRequestWithResponse[Req any, Resp any](
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
reqDTO Req,
|
reqDTO Req,
|
||||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
||||||
successMsg string,
|
successMsg string,
|
||||||
idExtractor idExtractorFunc,
|
idExtractor idExtractorFunc,
|
||||||
) {
|
) error {
|
||||||
// 1. 绑定请求体
|
// 1. 绑定请求体
|
||||||
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
|
if err := ctx.Bind(&reqDTO); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, fmt.Sprintf("无效的请求体: %v", err), action, fmt.Sprintf("请求体绑定失败: %v", err), reqDTO)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, fmt.Sprintf("无效的请求体: %v", err), action, fmt.Sprintf("请求体绑定失败: %v", err), reqDTO)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 提取操作员ID和主ID
|
// 2. 提取操作员ID和主ID
|
||||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 执行服务层逻辑
|
// 3. 执行服务层逻辑
|
||||||
respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mapAndSendError(c, ctx, action, err, primaryID)
|
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送成功响应
|
// 4. 发送成功响应
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体,但有路径参数和操作员ID,并返回响应DTO的API请求的通用逻辑。
|
// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体,但有路径参数和操作员ID,并返回响应DTO的API请求的通用逻辑。
|
||||||
func handleNoBodyAPIRequestWithResponse[Resp any](
|
func handleNoBodyAPIRequestWithResponse[Resp any](
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
|
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
|
||||||
successMsg string,
|
successMsg string,
|
||||||
idExtractor idExtractorFunc,
|
idExtractor idExtractorFunc,
|
||||||
) {
|
) error {
|
||||||
// 1. 提取操作员ID和主ID
|
// 1. 提取操作员ID和主ID
|
||||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 执行服务层逻辑
|
// 2. 执行服务层逻辑
|
||||||
respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
|
respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mapAndSendError(c, ctx, action, err, primaryID)
|
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发送成功响应
|
// 3. 发送成功响应
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
|
// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
|
||||||
func handleQueryAPIRequestWithResponse[Query any, Resp any](
|
func handleQueryAPIRequestWithResponse[Query any, Resp any](
|
||||||
c *PigBatchController,
|
c *PigBatchController,
|
||||||
ctx *gin.Context,
|
ctx echo.Context,
|
||||||
action string,
|
action string,
|
||||||
queryDTO Query,
|
queryDTO Query,
|
||||||
serviceExecutor func(ctx *gin.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
serviceExecutor func(ctx echo.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
||||||
successMsg string,
|
successMsg string,
|
||||||
) {
|
) error {
|
||||||
// 1. 绑定查询参数
|
// 1. 绑定查询参数
|
||||||
if err := ctx.ShouldBindQuery(&queryDTO); err != nil {
|
if err := ctx.Bind(&queryDTO); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取操作员ID
|
// 2. 获取操作员ID
|
||||||
operatorID, err := controller.GetOperatorIDFromContext(ctx)
|
operatorID, err := controller.GetOperatorIDFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 执行服务层逻辑
|
// 3. 执行服务层逻辑
|
||||||
respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
|
respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 对于列表查询,通常没有primaryID,所以传递0
|
// 对于列表查询,通常没有primaryID,所以传递0
|
||||||
mapAndSendError(c, ctx, action, err, 0)
|
return mapAndSendError(c, ctx, action, err, 0)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送成功响应
|
// 4. 发送成功响应
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PigBatchController 负责处理猪批次相关的API请求
|
// PigBatchController 负责处理猪批次相关的API请求
|
||||||
@@ -34,13 +34,13 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService)
|
|||||||
// @Param body body dto.PigBatchCreateDTO true "猪批次信息"
|
// @Param body body dto.PigBatchCreateDTO true "猪批次信息"
|
||||||
// @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
|
// @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
|
||||||
// @Router /api/v1/pig-batches [post]
|
// @Router /api/v1/pig-batches [post]
|
||||||
func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
|
func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
|
||||||
const action = "创建猪批次"
|
const action = "创建猪批次"
|
||||||
var req dto.PigBatchCreateDTO
|
var req dto.PigBatchCreateDTO
|
||||||
|
|
||||||
handleAPIRequestWithResponse(
|
return handleAPIRequestWithResponse(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||||
// 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成
|
// 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成
|
||||||
return c.service.CreatePigBatch(operatorID, req)
|
return c.service.CreatePigBatch(operatorID, req)
|
||||||
},
|
},
|
||||||
@@ -58,12 +58,12 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪批次ID"
|
// @Param id path int true "猪批次ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
|
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
|
||||||
// @Router /api/v1/pig-batches/{id} [get]
|
// @Router /api/v1/pig-batches/{id} [get]
|
||||||
func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
|
func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
|
||||||
const action = "获取猪批次"
|
const action = "获取猪批次"
|
||||||
|
|
||||||
handleNoBodyAPIRequestWithResponse(
|
return handleNoBodyAPIRequestWithResponse(
|
||||||
c, ctx, action,
|
c, ctx, action,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
|
func(ctx echo.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
|
||||||
return c.service.GetPigBatch(primaryID)
|
return c.service.GetPigBatch(primaryID)
|
||||||
},
|
},
|
||||||
"获取成功",
|
"获取成功",
|
||||||
@@ -82,13 +82,13 @@ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
|
|||||||
// @Param body body dto.PigBatchUpdateDTO true "猪批次信息"
|
// @Param body body dto.PigBatchUpdateDTO true "猪批次信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
|
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
|
||||||
// @Router /api/v1/pig-batches/{id} [put]
|
// @Router /api/v1/pig-batches/{id} [put]
|
||||||
func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
|
func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
|
||||||
const action = "更新猪批次"
|
const action = "更新猪批次"
|
||||||
var req dto.PigBatchUpdateDTO
|
var req dto.PigBatchUpdateDTO
|
||||||
|
|
||||||
handleAPIRequestWithResponse(
|
return handleAPIRequestWithResponse(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||||
return c.service.UpdatePigBatch(primaryID, req)
|
return c.service.UpdatePigBatch(primaryID, req)
|
||||||
},
|
},
|
||||||
"更新成功",
|
"更新成功",
|
||||||
@@ -105,12 +105,12 @@ func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪批次ID"
|
// @Param id path int true "猪批次ID"
|
||||||
// @Success 200 {object} controller.Response "删除成功"
|
// @Success 200 {object} controller.Response "删除成功"
|
||||||
// @Router /api/v1/pig-batches/{id} [delete]
|
// @Router /api/v1/pig-batches/{id} [delete]
|
||||||
func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
|
func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
|
||||||
const action = "删除猪批次"
|
const action = "删除猪批次"
|
||||||
|
|
||||||
handleNoBodyAPIRequest(
|
return handleNoBodyAPIRequest(
|
||||||
c, ctx, action,
|
c, ctx, action,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||||
return c.service.DeletePigBatch(primaryID)
|
return c.service.DeletePigBatch(primaryID)
|
||||||
},
|
},
|
||||||
"删除成功",
|
"删除成功",
|
||||||
@@ -127,13 +127,13 @@ func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
|
|||||||
// @Param is_active query bool false "是否活跃 (true/false)"
|
// @Param is_active query bool false "是否活跃 (true/false)"
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
|
// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
|
||||||
// @Router /api/v1/pig-batches [get]
|
// @Router /api/v1/pig-batches [get]
|
||||||
func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
|
func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
|
||||||
const action = "获取猪批次列表"
|
const action = "获取猪批次列表"
|
||||||
var query dto.PigBatchQueryDTO
|
var query dto.PigBatchQueryDTO
|
||||||
|
|
||||||
handleQueryAPIRequestWithResponse(
|
return handleQueryAPIRequestWithResponse(
|
||||||
c, ctx, action, &query,
|
c, ctx, action, &query,
|
||||||
func(ctx *gin.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
func(ctx echo.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
||||||
return c.service.ListPigBatches(query.IsActive)
|
return c.service.ListPigBatches(query.IsActive)
|
||||||
},
|
},
|
||||||
"获取成功",
|
"获取成功",
|
||||||
@@ -151,13 +151,13 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
|
|||||||
// @Param body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表"
|
// @Param body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表"
|
||||||
// @Success 200 {object} controller.Response "分配成功"
|
// @Success 200 {object} controller.Response "分配成功"
|
||||||
// @Router /api/v1/pig-batches/assign-pens/{id} [post]
|
// @Router /api/v1/pig-batches/assign-pens/{id} [post]
|
||||||
func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
|
func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
|
||||||
const action = "为猪批次分配空栏"
|
const action = "为猪批次分配空栏"
|
||||||
var req dto.AssignEmptyPensToBatchRequest
|
var req dto.AssignEmptyPensToBatchRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
|
||||||
return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
|
return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
|
||||||
},
|
},
|
||||||
"分配成功",
|
"分配成功",
|
||||||
@@ -176,18 +176,18 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
|
|||||||
// @Param body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)"
|
// @Param body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)"
|
||||||
// @Success 200 {object} controller.Response "划拨成功"
|
// @Success 200 {object} controller.Response "划拨成功"
|
||||||
// @Router /api/v1/pig-batches/reclassify-pen/{fromBatchID} [post]
|
// @Router /api/v1/pig-batches/reclassify-pen/{fromBatchID} [post]
|
||||||
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
|
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
|
||||||
const action = "划拨猪栏到新批次"
|
const action = "划拨猪栏到新批次"
|
||||||
var req dto.ReclassifyPenToNewBatchRequest
|
var req dto.ReclassifyPenToNewBatchRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
|
||||||
// primaryID 在这里是 fromBatchID
|
// primaryID 在这里是 fromBatchID
|
||||||
return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
|
return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
|
||||||
},
|
},
|
||||||
"划拨成功",
|
"划拨成功",
|
||||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
||||||
idParam := ctx.Param("fromBatchID")
|
idParam := ctx.Param("fromBatchID")
|
||||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -208,22 +208,22 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
|
|||||||
// @Param penID path int true "待移除的猪栏ID"
|
// @Param penID path int true "待移除的猪栏ID"
|
||||||
// @Success 200 {object} controller.Response "移除成功"
|
// @Success 200 {object} controller.Response "移除成功"
|
||||||
// @Router /api/v1/pig-batches/remove-pen/{penID}/{batchID} [delete]
|
// @Router /api/v1/pig-batches/remove-pen/{penID}/{batchID} [delete]
|
||||||
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
|
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
|
||||||
const action = "从猪批次移除空栏"
|
const action = "从猪批次移除空栏"
|
||||||
|
|
||||||
handleNoBodyAPIRequest(
|
return handleNoBodyAPIRequest(
|
||||||
c, ctx, action,
|
c, ctx, action,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||||
// primaryID 在这里是 batchID
|
// primaryID 在这里是 batchID
|
||||||
penIDParam := ctx.Param("penID")
|
penIDParam := ctx.Param("penID")
|
||||||
penID, err := strconv.ParseUint(penIDParam, 10, 32)
|
parsedPenID, err := strconv.ParseUint(penIDParam, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // 返回错误,因为 penID 格式无效
|
return err // 返回错误,因为 penID 格式无效
|
||||||
}
|
}
|
||||||
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(penID))
|
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(parsedPenID))
|
||||||
},
|
},
|
||||||
"移除成功",
|
"移除成功",
|
||||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
||||||
idParam := ctx.Param("batchID")
|
idParam := ctx.Param("batchID")
|
||||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,13 +245,13 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
|
|||||||
// @Param body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)"
|
// @Param body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)"
|
||||||
// @Success 200 {object} controller.Response "移入成功"
|
// @Success 200 {object} controller.Response "移入成功"
|
||||||
// @Router /api/v1/pig-batches/move-pigs-into-pen/{id} [post]
|
// @Router /api/v1/pig-batches/move-pigs-into-pen/{id} [post]
|
||||||
func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) {
|
func (c *PigBatchController) MovePigsIntoPen(ctx echo.Context) error {
|
||||||
const action = "将猪只移入猪栏"
|
const action = "将猪只移入猪栏"
|
||||||
var req dto.MovePigsIntoPenRequest
|
var req dto.MovePigsIntoPenRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
|
||||||
return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||||
},
|
},
|
||||||
"移入成功",
|
"移入成功",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package management
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecordSickPigs godoc
|
// RecordSickPigs godoc
|
||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
// @Param body body dto.RecordSickPigsRequest true "记录病猪请求信息"
|
// @Param body body dto.RecordSickPigsRequest true "记录病猪请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-sick-pigs/{id} [post]
|
// @Router /api/v1/pig-batches/record-sick-pigs/{id} [post]
|
||||||
func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
|
func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
|
||||||
const action = "记录新增病猪事件"
|
const action = "记录新增病猪事件"
|
||||||
var req dto.RecordSickPigsRequest
|
var req dto.RecordSickPigsRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
|
||||||
return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
@@ -41,13 +41,13 @@ func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
|
|||||||
// @Param body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息"
|
// @Param body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-sick-pig-recovery/{id} [post]
|
// @Router /api/v1/pig-batches/record-sick-pig-recovery/{id} [post]
|
||||||
func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
|
func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
|
||||||
const action = "记录病猪康复事件"
|
const action = "记录病猪康复事件"
|
||||||
var req dto.RecordSickPigRecoveryRequest
|
var req dto.RecordSickPigRecoveryRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
|
||||||
return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
@@ -66,13 +66,13 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
|
|||||||
// @Param body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息"
|
// @Param body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-sick-pig-death/{id} [post]
|
// @Router /api/v1/pig-batches/record-sick-pig-death/{id} [post]
|
||||||
func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
|
func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
|
||||||
const action = "记录病猪死亡事件"
|
const action = "记录病猪死亡事件"
|
||||||
var req dto.RecordSickPigDeathRequest
|
var req dto.RecordSickPigDeathRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
|
||||||
return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
@@ -91,13 +91,13 @@ func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
|
|||||||
// @Param body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息"
|
// @Param body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-sick-pig-cull/{id} [post]
|
// @Router /api/v1/pig-batches/record-sick-pig-cull/{id} [post]
|
||||||
func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
|
func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
|
||||||
const action = "记录病猪淘汰事件"
|
const action = "记录病猪淘汰事件"
|
||||||
var req dto.RecordSickPigCullRequest
|
var req dto.RecordSickPigCullRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
|
||||||
return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
@@ -116,13 +116,13 @@ func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
|
|||||||
// @Param body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息"
|
// @Param body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-death/{id} [post]
|
// @Router /api/v1/pig-batches/record-death/{id} [post]
|
||||||
func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
|
func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
|
||||||
const action = "记录正常猪只死亡事件"
|
const action = "记录正常猪只死亡事件"
|
||||||
var req dto.RecordDeathRequest
|
var req dto.RecordDeathRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
|
||||||
return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
@@ -141,13 +141,13 @@ func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
|
|||||||
// @Param body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息"
|
// @Param body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息"
|
||||||
// @Success 200 {object} controller.Response "记录成功"
|
// @Success 200 {object} controller.Response "记录成功"
|
||||||
// @Router /api/v1/pig-batches/record-cull/{id} [post]
|
// @Router /api/v1/pig-batches/record-cull/{id} [post]
|
||||||
func (c *PigBatchController) RecordCull(ctx *gin.Context) {
|
func (c *PigBatchController) RecordCull(ctx echo.Context) error {
|
||||||
const action = "记录正常猪只淘汰事件"
|
const action = "记录正常猪只淘汰事件"
|
||||||
var req dto.RecordCullRequest
|
var req dto.RecordCullRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
|
||||||
return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||||
},
|
},
|
||||||
"记录成功",
|
"记录成功",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package management
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SellPigs godoc
|
// SellPigs godoc
|
||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
// @Param body body dto.SellPigsRequest true "卖猪请求信息"
|
// @Param body body dto.SellPigsRequest true "卖猪请求信息"
|
||||||
// @Success 200 {object} controller.Response "卖猪成功"
|
// @Success 200 {object} controller.Response "卖猪成功"
|
||||||
// @Router /api/v1/pig-batches/sell-pigs/{id} [post]
|
// @Router /api/v1/pig-batches/sell-pigs/{id} [post]
|
||||||
func (c *PigBatchController) SellPigs(ctx *gin.Context) {
|
func (c *PigBatchController) SellPigs(ctx echo.Context) error {
|
||||||
const action = "卖猪"
|
const action = "卖猪"
|
||||||
var req dto.SellPigsRequest
|
var req dto.SellPigsRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
|
||||||
return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||||
},
|
},
|
||||||
"卖猪成功",
|
"卖猪成功",
|
||||||
@@ -41,13 +41,13 @@ func (c *PigBatchController) SellPigs(ctx *gin.Context) {
|
|||||||
// @Param body body dto.BuyPigsRequest true "买猪请求信息"
|
// @Param body body dto.BuyPigsRequest true "买猪请求信息"
|
||||||
// @Success 200 {object} controller.Response "买猪成功"
|
// @Success 200 {object} controller.Response "买猪成功"
|
||||||
// @Router /api/v1/pig-batches/buy-pigs/{id} [post]
|
// @Router /api/v1/pig-batches/buy-pigs/{id} [post]
|
||||||
func (c *PigBatchController) BuyPigs(ctx *gin.Context) {
|
func (c *PigBatchController) BuyPigs(ctx echo.Context) error {
|
||||||
const action = "买猪"
|
const action = "买猪"
|
||||||
var req dto.BuyPigsRequest
|
var req dto.BuyPigsRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
|
||||||
return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||||
},
|
},
|
||||||
"买猪成功",
|
"买猪成功",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransferPigsAcrossBatches godoc
|
// TransferPigsAcrossBatches godoc
|
||||||
@@ -18,18 +18,18 @@ import (
|
|||||||
// @Param body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息"
|
// @Param body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息"
|
||||||
// @Success 200 {object} controller.Response "调栏成功"
|
// @Success 200 {object} controller.Response "调栏成功"
|
||||||
// @Router /api/v1/pig-batches/transfer-across-batches/{sourceBatchID} [post]
|
// @Router /api/v1/pig-batches/transfer-across-batches/{sourceBatchID} [post]
|
||||||
func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
|
func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
|
||||||
const action = "跨猪群调栏"
|
const action = "跨猪群调栏"
|
||||||
var req dto.TransferPigsAcrossBatchesRequest
|
var req dto.TransferPigsAcrossBatchesRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
|
||||||
// primaryID 在这里是 sourceBatchID
|
// primaryID 在这里是 sourceBatchID
|
||||||
return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||||
},
|
},
|
||||||
"调栏成功",
|
"调栏成功",
|
||||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
||||||
idParam := ctx.Param("sourceBatchID")
|
idParam := ctx.Param("sourceBatchID")
|
||||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,13 +51,13 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
|
|||||||
// @Param body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息"
|
// @Param body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息"
|
||||||
// @Success 200 {object} controller.Response "调栏成功"
|
// @Success 200 {object} controller.Response "调栏成功"
|
||||||
// @Router /api/v1/pig-batches/transfer-within-batch/{id} [post]
|
// @Router /api/v1/pig-batches/transfer-within-batch/{id} [post]
|
||||||
func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) {
|
func (c *PigBatchController) TransferPigsWithinBatch(ctx echo.Context) error {
|
||||||
const action = "群内调栏"
|
const action = "群内调栏"
|
||||||
var req dto.TransferPigsWithinBatchRequest
|
var req dto.TransferPigsWithinBatchRequest
|
||||||
|
|
||||||
handleAPIRequest(
|
return handleAPIRequest(
|
||||||
c, ctx, action, &req,
|
c, ctx, action, &req,
|
||||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
|
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
|
||||||
// primaryID 在这里是 batchID
|
// primaryID 在这里是 batchID
|
||||||
return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- 控制器定义 ---
|
// --- 控制器定义 ---
|
||||||
@@ -31,7 +31,7 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
|
|||||||
|
|
||||||
// CreatePigHouse godoc
|
// CreatePigHouse godoc
|
||||||
// @Summary 创建猪舍
|
// @Summary 创建猪舍
|
||||||
// @Description 创建一个新的猪舍
|
// @Description 根据提供的信息创建一个新猪舍
|
||||||
// @Tags 猪场管理
|
// @Tags 猪场管理
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -39,19 +39,18 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
|
|||||||
// @Param body body dto.CreatePigHouseRequest true "猪舍信息"
|
// @Param body body dto.CreatePigHouseRequest true "猪舍信息"
|
||||||
// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
|
// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
|
||||||
// @Router /api/v1/pig-houses [post]
|
// @Router /api/v1/pig-houses [post]
|
||||||
func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
|
func (c *PigFarmController) CreatePigHouse(ctx echo.Context) error {
|
||||||
const action = "创建猪舍"
|
const action = "创建猪舍"
|
||||||
var req dto.CreatePigHouseRequest
|
var req dto.CreatePigHouseRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
c.logger.Errorf("%s: 参数绑定失败: %v", action, err)
|
||||||
return
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
house, err := c.service.CreatePigHouse(req.Name, req.Description)
|
house, err := c.service.CreatePigHouse(req.Name, req.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PigHouseResponse{
|
resp := dto.PigHouseResponse{
|
||||||
@@ -59,7 +58,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
|
|||||||
Name: house.Name,
|
Name: house.Name,
|
||||||
Description: house.Description,
|
Description: house.Description,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPigHouse godoc
|
// GetPigHouse godoc
|
||||||
@@ -71,23 +70,20 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪舍ID"
|
// @Param id path int true "猪舍ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
|
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
|
||||||
// @Router /api/v1/pig-houses/{id} [get]
|
// @Router /api/v1/pig-houses/{id} [get]
|
||||||
func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
|
func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
|
||||||
const action = "获取猪舍"
|
const action = "获取猪舍"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
house, err := c.service.GetPigHouseByID(uint(id))
|
house, err := c.service.GetPigHouseByID(uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrHouseNotFound) {
|
if errors.Is(err, service.ErrHouseNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PigHouseResponse{
|
resp := dto.PigHouseResponse{
|
||||||
@@ -95,7 +91,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
|
|||||||
Name: house.Name,
|
Name: house.Name,
|
||||||
Description: house.Description,
|
Description: house.Description,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigHouses godoc
|
// ListPigHouses godoc
|
||||||
@@ -106,13 +102,12 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
|
// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
|
||||||
// @Router /api/v1/pig-houses [get]
|
// @Router /api/v1/pig-houses [get]
|
||||||
func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
|
func (c *PigFarmController) ListPigHouses(ctx echo.Context) error {
|
||||||
const action = "获取猪舍列表"
|
const action = "获取猪舍列表"
|
||||||
houses, err := c.service.ListPigHouses()
|
houses, err := c.service.ListPigHouses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp []dto.PigHouseResponse
|
var resp []dto.PigHouseResponse
|
||||||
@@ -124,7 +119,7 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePigHouse godoc
|
// UpdatePigHouse godoc
|
||||||
@@ -138,29 +133,25 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
|
|||||||
// @Param body body dto.UpdatePigHouseRequest true "猪舍信息"
|
// @Param body body dto.UpdatePigHouseRequest true "猪舍信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
|
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
|
||||||
// @Router /api/v1/pig-houses/{id} [put]
|
// @Router /api/v1/pig-houses/{id} [put]
|
||||||
func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
|
func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
|
||||||
const action = "更新猪舍"
|
const action = "更新猪舍"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdatePigHouseRequest
|
var req dto.UpdatePigHouseRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
|
house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrHouseNotFound) {
|
if errors.Is(err, service.ErrHouseNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PigHouseResponse{
|
resp := dto.PigHouseResponse{
|
||||||
@@ -168,7 +159,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
|
|||||||
Name: house.Name,
|
Name: house.Name,
|
||||||
Description: house.Description,
|
Description: house.Description,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePigHouse godoc
|
// DeletePigHouse godoc
|
||||||
@@ -180,30 +171,26 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪舍ID"
|
// @Param id path int true "猪舍ID"
|
||||||
// @Success 200 {object} controller.Response "删除成功"
|
// @Success 200 {object} controller.Response "删除成功"
|
||||||
// @Router /api/v1/pig-houses/{id} [delete]
|
// @Router /api/v1/pig-houses/{id} [delete]
|
||||||
func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
|
func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
|
||||||
const action = "删除猪舍"
|
const action = "删除猪舍"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.service.DeletePigHouse(uint(id)); err != nil {
|
if err := c.service.DeletePigHouse(uint(id)); err != nil {
|
||||||
if errors.Is(err, service.ErrHouseNotFound) {
|
if errors.Is(err, service.ErrHouseNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 检查是否是业务逻辑错误
|
// 检查是否是业务逻辑错误
|
||||||
if errors.Is(err, service.ErrHouseContainsPens) {
|
if errors.Is(err, service.ErrHouseContainsPens) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 猪栏 (Pen) API 实现 ---
|
// --- 猪栏 (Pen) API 实现 ---
|
||||||
@@ -218,24 +205,21 @@ func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
|
|||||||
// @Param body body dto.CreatePenRequest true "猪栏信息"
|
// @Param body body dto.CreatePenRequest true "猪栏信息"
|
||||||
// @Success 201 {object} controller.Response{data=dto.PenResponse} "创建成功"
|
// @Success 201 {object} controller.Response{data=dto.PenResponse} "创建成功"
|
||||||
// @Router /api/v1/pens [post]
|
// @Router /api/v1/pens [post]
|
||||||
func (c *PigFarmController) CreatePen(ctx *gin.Context) {
|
func (c *PigFarmController) CreatePen(ctx echo.Context) error {
|
||||||
const action = "创建猪栏"
|
const action = "创建猪栏"
|
||||||
var req dto.CreatePenRequest
|
var req dto.CreatePenRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
|
pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 检查是否是业务逻辑错误
|
// 检查是否是业务逻辑错误
|
||||||
if errors.Is(err, service.ErrHouseNotFound) {
|
if errors.Is(err, service.ErrHouseNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PenResponse{
|
resp := dto.PenResponse{
|
||||||
@@ -245,7 +229,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) {
|
|||||||
Capacity: pen.Capacity,
|
Capacity: pen.Capacity,
|
||||||
Status: pen.Status,
|
Status: pen.Status,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPen godoc
|
// GetPen godoc
|
||||||
@@ -257,26 +241,23 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪栏ID"
|
// @Param id path int true "猪栏ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功"
|
// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功"
|
||||||
// @Router /api/v1/pens/{id} [get]
|
// @Router /api/v1/pens/{id} [get]
|
||||||
func (c *PigFarmController) GetPen(ctx *gin.Context) {
|
func (c *PigFarmController) GetPen(ctx echo.Context) error {
|
||||||
const action = "获取猪栏"
|
const action = "获取猪栏"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pen, err := c.service.GetPenByID(uint(id))
|
pen, err := c.service.GetPenByID(uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrPenNotFound) {
|
if errors.Is(err, service.ErrPenNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pen, action, "获取成功", pen)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pen, action, "获取成功", pen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPens godoc
|
// ListPens godoc
|
||||||
@@ -287,16 +268,15 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
|
// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
|
||||||
// @Router /api/v1/pens [get]
|
// @Router /api/v1/pens [get]
|
||||||
func (c *PigFarmController) ListPens(ctx *gin.Context) {
|
func (c *PigFarmController) ListPens(ctx echo.Context) error {
|
||||||
const action = "获取猪栏列表"
|
const action = "获取猪栏列表"
|
||||||
pens, err := c.service.ListPens()
|
pens, err := c.service.ListPens()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pens, action, "获取成功", pens)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pens, action, "获取成功", pens)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePen godoc
|
// UpdatePen godoc
|
||||||
@@ -310,30 +290,26 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) {
|
|||||||
// @Param body body dto.UpdatePenRequest true "猪栏信息"
|
// @Param body body dto.UpdatePenRequest true "猪栏信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
||||||
// @Router /api/v1/pens/{id} [put]
|
// @Router /api/v1/pens/{id} [put]
|
||||||
func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
|
func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
|
||||||
const action = "更新猪栏"
|
const action = "更新猪栏"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdatePenRequest
|
var req dto.UpdatePenRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
|
pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrPenNotFound) {
|
if errors.Is(err, service.ErrPenNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 其他业务逻辑错误可以在这里添加处理
|
// 其他业务逻辑错误可以在这里添加处理
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PenResponse{
|
resp := dto.PenResponse{
|
||||||
@@ -344,7 +320,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
|
|||||||
Status: pen.Status,
|
Status: pen.Status,
|
||||||
PigBatchID: pen.PigBatchID,
|
PigBatchID: pen.PigBatchID,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePen godoc
|
// DeletePen godoc
|
||||||
@@ -356,30 +332,26 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
|
|||||||
// @Param id path int true "猪栏ID"
|
// @Param id path int true "猪栏ID"
|
||||||
// @Success 200 {object} controller.Response "删除成功"
|
// @Success 200 {object} controller.Response "删除成功"
|
||||||
// @Router /api/v1/pens/{id} [delete]
|
// @Router /api/v1/pens/{id} [delete]
|
||||||
func (c *PigFarmController) DeletePen(ctx *gin.Context) {
|
func (c *PigFarmController) DeletePen(ctx echo.Context) error {
|
||||||
const action = "删除猪栏"
|
const action = "删除猪栏"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.service.DeletePen(uint(id)); err != nil {
|
if err := c.service.DeletePen(uint(id)); err != nil {
|
||||||
if errors.Is(err, service.ErrPenNotFound) {
|
if errors.Is(err, service.ErrPenNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 检查是否是业务逻辑错误
|
// 检查是否是业务逻辑错误
|
||||||
if errors.Is(err, service.ErrPenInUse) {
|
if errors.Is(err, service.ErrPenInUse) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePenStatus godoc
|
// UpdatePenStatus godoc
|
||||||
@@ -393,32 +365,27 @@ func (c *PigFarmController) DeletePen(ctx *gin.Context) {
|
|||||||
// @Param body body dto.UpdatePenStatusRequest true "新的猪栏状态"
|
// @Param body body dto.UpdatePenStatusRequest true "新的猪栏状态"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
||||||
// @Router /api/v1/pens/{id}/status [put]
|
// @Router /api/v1/pens/{id}/status [put]
|
||||||
func (c *PigFarmController) UpdatePenStatus(ctx *gin.Context) {
|
func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
|
||||||
const action = "更新猪栏状态"
|
const action = "更新猪栏状态"
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.UpdatePenStatusRequest
|
var req dto.UpdatePenStatusRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
|
pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrPenNotFound) {
|
if errors.Is(err, service.ErrPenNotFound) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||||
return
|
|
||||||
} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
|
} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.PenResponse{
|
resp := dto.PenResponse{
|
||||||
@@ -429,5 +396,5 @@ func (c *PigFarmController) UpdatePenStatus(ctx *gin.Context) {
|
|||||||
Status: pen.Status,
|
Status: pen.Status,
|
||||||
PigBatchID: pen.PigBatchID,
|
PigBatchID: pen.PigBatchID,
|
||||||
}
|
}
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller 监控控制器,封装了所有与数据监控相关的业务逻辑
|
// Controller 监控控制器,封装了所有与数据监控相关的业务逻辑
|
||||||
@@ -35,14 +35,13 @@ func NewController(monitorService service.MonitorService, logger *logs.Logger) *
|
|||||||
// @Param query query dto.ListSensorDataRequest true "查询参数"
|
// @Param query query dto.ListSensorDataRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListSensorDataResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListSensorDataResponse}
|
||||||
// @Router /api/v1/monitor/sensor-data [get]
|
// @Router /api/v1/monitor/sensor-data [get]
|
||||||
func (c *Controller) ListSensorData(ctx *gin.Context) {
|
func (c *Controller) ListSensorData(ctx echo.Context) error {
|
||||||
const actionType = "获取传感器数据列表"
|
const actionType = "获取传感器数据列表"
|
||||||
|
|
||||||
var req dto.ListSensorDataRequest
|
var req dto.ListSensorDataRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.SensorDataListOptions{
|
opts := repository.SensorDataListOptions{
|
||||||
@@ -60,18 +59,16 @@ func (c *Controller) ListSensorData(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDeviceCommandLogs godoc
|
// ListDeviceCommandLogs godoc
|
||||||
@@ -83,14 +80,13 @@ func (c *Controller) ListSensorData(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListDeviceCommandLogRequest true "查询参数"
|
// @Param query query dto.ListDeviceCommandLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListDeviceCommandLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListDeviceCommandLogResponse}
|
||||||
// @Router /api/v1/monitor/device-command-logs [get]
|
// @Router /api/v1/monitor/device-command-logs [get]
|
||||||
func (c *Controller) ListDeviceCommandLogs(ctx *gin.Context) {
|
func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取设备命令日志列表"
|
const actionType = "获取设备命令日志列表"
|
||||||
|
|
||||||
var req dto.ListDeviceCommandLogRequest
|
var req dto.ListDeviceCommandLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.DeviceCommandLogListOptions{
|
opts := repository.DeviceCommandLogListOptions{
|
||||||
@@ -105,18 +101,16 @@ func (c *Controller) ListDeviceCommandLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPlanExecutionLogs godoc
|
// ListPlanExecutionLogs godoc
|
||||||
@@ -128,14 +122,13 @@ func (c *Controller) ListDeviceCommandLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPlanExecutionLogRequest true "查询参数"
|
// @Param query query dto.ListPlanExecutionLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPlanExecutionLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPlanExecutionLogResponse}
|
||||||
// @Router /api/v1/monitor/plan-execution-logs [get]
|
// @Router /api/v1/monitor/plan-execution-logs [get]
|
||||||
func (c *Controller) ListPlanExecutionLogs(ctx *gin.Context) {
|
func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取计划执行日志列表"
|
const actionType = "获取计划执行日志列表"
|
||||||
|
|
||||||
var req dto.ListPlanExecutionLogRequest
|
var req dto.ListPlanExecutionLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PlanExecutionLogListOptions{
|
opts := repository.PlanExecutionLogListOptions{
|
||||||
@@ -153,18 +146,16 @@ func (c *Controller) ListPlanExecutionLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPlanExecutionLogResponse(planLogs, plans, total, req.Page, req.PageSize)
|
resp := dto.NewListPlanExecutionLogResponse(planLogs, plans, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(planLogs), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(planLogs), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTaskExecutionLogs godoc
|
// ListTaskExecutionLogs godoc
|
||||||
@@ -176,14 +167,13 @@ func (c *Controller) ListPlanExecutionLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListTaskExecutionLogRequest true "查询参数"
|
// @Param query query dto.ListTaskExecutionLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListTaskExecutionLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListTaskExecutionLogResponse}
|
||||||
// @Router /api/v1/monitor/task-execution-logs [get]
|
// @Router /api/v1/monitor/task-execution-logs [get]
|
||||||
func (c *Controller) ListTaskExecutionLogs(ctx *gin.Context) {
|
func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取任务执行日志列表"
|
const actionType = "获取任务执行日志列表"
|
||||||
|
|
||||||
var req dto.ListTaskExecutionLogRequest
|
var req dto.ListTaskExecutionLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.TaskExecutionLogListOptions{
|
opts := repository.TaskExecutionLogListOptions{
|
||||||
@@ -202,18 +192,16 @@ func (c *Controller) ListTaskExecutionLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPendingCollections godoc
|
// ListPendingCollections godoc
|
||||||
@@ -225,14 +213,13 @@ func (c *Controller) ListTaskExecutionLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPendingCollectionRequest true "查询参数"
|
// @Param query query dto.ListPendingCollectionRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPendingCollectionResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPendingCollectionResponse}
|
||||||
// @Router /api/v1/monitor/pending-collections [get]
|
// @Router /api/v1/monitor/pending-collections [get]
|
||||||
func (c *Controller) ListPendingCollections(ctx *gin.Context) {
|
func (c *Controller) ListPendingCollections(ctx echo.Context) error {
|
||||||
const actionType = "获取待采集请求列表"
|
const actionType = "获取待采集请求列表"
|
||||||
|
|
||||||
var req dto.ListPendingCollectionRequest
|
var req dto.ListPendingCollectionRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PendingCollectionListOptions{
|
opts := repository.PendingCollectionListOptions{
|
||||||
@@ -250,18 +237,16 @@ func (c *Controller) ListPendingCollections(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUserActionLogs godoc
|
// ListUserActionLogs godoc
|
||||||
@@ -273,14 +258,13 @@ func (c *Controller) ListPendingCollections(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListUserActionLogRequest true "查询参数"
|
// @Param query query dto.ListUserActionLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse}
|
||||||
// @Router /api/v1/monitor/user-action-logs [get]
|
// @Router /api/v1/monitor/user-action-logs [get]
|
||||||
func (c *Controller) ListUserActionLogs(ctx *gin.Context) {
|
func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取用户操作日志列表"
|
const actionType = "获取用户操作日志列表"
|
||||||
|
|
||||||
var req dto.ListUserActionLogRequest
|
var req dto.ListUserActionLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.UserActionLogListOptions{
|
opts := repository.UserActionLogListOptions{
|
||||||
@@ -300,18 +284,16 @@ func (c *Controller) ListUserActionLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRawMaterialPurchases godoc
|
// ListRawMaterialPurchases godoc
|
||||||
@@ -323,14 +305,13 @@ func (c *Controller) ListUserActionLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListRawMaterialPurchaseRequest true "查询参数"
|
// @Param query query dto.ListRawMaterialPurchaseRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
|
||||||
// @Router /api/v1/monitor/raw-material-purchases [get]
|
// @Router /api/v1/monitor/raw-material-purchases [get]
|
||||||
func (c *Controller) ListRawMaterialPurchases(ctx *gin.Context) {
|
func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
|
||||||
const actionType = "获取原料采购记录列表"
|
const actionType = "获取原料采购记录列表"
|
||||||
|
|
||||||
var req dto.ListRawMaterialPurchaseRequest
|
var req dto.ListRawMaterialPurchaseRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.RawMaterialPurchaseListOptions{
|
opts := repository.RawMaterialPurchaseListOptions{
|
||||||
@@ -345,18 +326,16 @@ func (c *Controller) ListRawMaterialPurchases(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRawMaterialStockLogs godoc
|
// ListRawMaterialStockLogs godoc
|
||||||
@@ -368,14 +347,13 @@ func (c *Controller) ListRawMaterialPurchases(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListRawMaterialStockLogRequest true "查询参数"
|
// @Param query query dto.ListRawMaterialStockLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
|
||||||
// @Router /api/v1/monitor/raw-material-stock-logs [get]
|
// @Router /api/v1/monitor/raw-material-stock-logs [get]
|
||||||
func (c *Controller) ListRawMaterialStockLogs(ctx *gin.Context) {
|
func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取原料库存日志列表"
|
const actionType = "获取原料库存日志列表"
|
||||||
|
|
||||||
var req dto.ListRawMaterialStockLogRequest
|
var req dto.ListRawMaterialStockLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.RawMaterialStockLogListOptions{
|
opts := repository.RawMaterialStockLogListOptions{
|
||||||
@@ -394,18 +372,16 @@ func (c *Controller) ListRawMaterialStockLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFeedUsageRecords godoc
|
// ListFeedUsageRecords godoc
|
||||||
@@ -417,14 +393,13 @@ func (c *Controller) ListRawMaterialStockLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListFeedUsageRecordRequest true "查询参数"
|
// @Param query query dto.ListFeedUsageRecordRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
|
||||||
// @Router /api/v1/monitor/feed-usage-records [get]
|
// @Router /api/v1/monitor/feed-usage-records [get]
|
||||||
func (c *Controller) ListFeedUsageRecords(ctx *gin.Context) {
|
func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
|
||||||
const actionType = "获取饲料使用记录列表"
|
const actionType = "获取饲料使用记录列表"
|
||||||
|
|
||||||
var req dto.ListFeedUsageRecordRequest
|
var req dto.ListFeedUsageRecordRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.FeedUsageRecordListOptions{
|
opts := repository.FeedUsageRecordListOptions{
|
||||||
@@ -440,18 +415,16 @@ func (c *Controller) ListFeedUsageRecords(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListMedicationLogs godoc
|
// ListMedicationLogs godoc
|
||||||
@@ -463,14 +436,13 @@ func (c *Controller) ListFeedUsageRecords(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListMedicationLogRequest true "查询参数"
|
// @Param query query dto.ListMedicationLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListMedicationLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListMedicationLogResponse}
|
||||||
// @Router /api/v1/monitor/medication-logs [get]
|
// @Router /api/v1/monitor/medication-logs [get]
|
||||||
func (c *Controller) ListMedicationLogs(ctx *gin.Context) {
|
func (c *Controller) ListMedicationLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取用药记录列表"
|
const actionType = "获取用药记录列表"
|
||||||
|
|
||||||
var req dto.ListMedicationLogRequest
|
var req dto.ListMedicationLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.MedicationLogListOptions{
|
opts := repository.MedicationLogListOptions{
|
||||||
@@ -490,18 +462,16 @@ func (c *Controller) ListMedicationLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigBatchLogs godoc
|
// ListPigBatchLogs godoc
|
||||||
@@ -513,14 +483,13 @@ func (c *Controller) ListMedicationLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPigBatchLogRequest true "查询参数"
|
// @Param query query dto.ListPigBatchLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPigBatchLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPigBatchLogResponse}
|
||||||
// @Router /api/v1/monitor/pig-batch-logs [get]
|
// @Router /api/v1/monitor/pig-batch-logs [get]
|
||||||
func (c *Controller) ListPigBatchLogs(ctx *gin.Context) {
|
func (c *Controller) ListPigBatchLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取猪批次日志列表"
|
const actionType = "获取猪批次日志列表"
|
||||||
|
|
||||||
var req dto.ListPigBatchLogRequest
|
var req dto.ListPigBatchLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PigBatchLogListOptions{
|
opts := repository.PigBatchLogListOptions{
|
||||||
@@ -539,18 +508,16 @@ func (c *Controller) ListPigBatchLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListWeighingBatches godoc
|
// ListWeighingBatches godoc
|
||||||
@@ -562,14 +529,13 @@ func (c *Controller) ListPigBatchLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListWeighingBatchRequest true "查询参数"
|
// @Param query query dto.ListWeighingBatchRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListWeighingBatchResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListWeighingBatchResponse}
|
||||||
// @Router /api/v1/monitor/weighing-batches [get]
|
// @Router /api/v1/monitor/weighing-batches [get]
|
||||||
func (c *Controller) ListWeighingBatches(ctx *gin.Context) {
|
func (c *Controller) ListWeighingBatches(ctx echo.Context) error {
|
||||||
const actionType = "获取批次称重记录列表"
|
const actionType = "获取批次称重记录列表"
|
||||||
|
|
||||||
var req dto.ListWeighingBatchRequest
|
var req dto.ListWeighingBatchRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.WeighingBatchListOptions{
|
opts := repository.WeighingBatchListOptions{
|
||||||
@@ -583,18 +549,16 @@ func (c *Controller) ListWeighingBatches(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListWeighingRecords godoc
|
// ListWeighingRecords godoc
|
||||||
@@ -606,14 +570,13 @@ func (c *Controller) ListWeighingBatches(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListWeighingRecordRequest true "查询参数"
|
// @Param query query dto.ListWeighingRecordRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListWeighingRecordResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListWeighingRecordResponse}
|
||||||
// @Router /api/v1/monitor/weighing-records [get]
|
// @Router /api/v1/monitor/weighing-records [get]
|
||||||
func (c *Controller) ListWeighingRecords(ctx *gin.Context) {
|
func (c *Controller) ListWeighingRecords(ctx echo.Context) error {
|
||||||
const actionType = "获取单次称重记录列表"
|
const actionType = "获取单次称重记录列表"
|
||||||
|
|
||||||
var req dto.ListWeighingRecordRequest
|
var req dto.ListWeighingRecordRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.WeighingRecordListOptions{
|
opts := repository.WeighingRecordListOptions{
|
||||||
@@ -629,18 +592,16 @@ func (c *Controller) ListWeighingRecords(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigTransferLogs godoc
|
// ListPigTransferLogs godoc
|
||||||
@@ -652,14 +613,13 @@ func (c *Controller) ListWeighingRecords(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPigTransferLogRequest true "查询参数"
|
// @Param query query dto.ListPigTransferLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPigTransferLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPigTransferLogResponse}
|
||||||
// @Router /api/v1/monitor/pig-transfer-logs [get]
|
// @Router /api/v1/monitor/pig-transfer-logs [get]
|
||||||
func (c *Controller) ListPigTransferLogs(ctx *gin.Context) {
|
func (c *Controller) ListPigTransferLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取猪只迁移日志列表"
|
const actionType = "获取猪只迁移日志列表"
|
||||||
|
|
||||||
var req dto.ListPigTransferLogRequest
|
var req dto.ListPigTransferLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PigTransferLogListOptions{
|
opts := repository.PigTransferLogListOptions{
|
||||||
@@ -680,18 +640,16 @@ func (c *Controller) ListPigTransferLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigSickLogs godoc
|
// ListPigSickLogs godoc
|
||||||
@@ -703,14 +661,13 @@ func (c *Controller) ListPigTransferLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPigSickLogRequest true "查询参数"
|
// @Param query query dto.ListPigSickLogRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPigSickLogResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPigSickLogResponse}
|
||||||
// @Router /api/v1/monitor/pig-sick-logs [get]
|
// @Router /api/v1/monitor/pig-sick-logs [get]
|
||||||
func (c *Controller) ListPigSickLogs(ctx *gin.Context) {
|
func (c *Controller) ListPigSickLogs(ctx echo.Context) error {
|
||||||
const actionType = "获取病猪日志列表"
|
const actionType = "获取病猪日志列表"
|
||||||
|
|
||||||
var req dto.ListPigSickLogRequest
|
var req dto.ListPigSickLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PigSickLogListOptions{
|
opts := repository.PigSickLogListOptions{
|
||||||
@@ -734,18 +691,16 @@ func (c *Controller) ListPigSickLogs(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigPurchases godoc
|
// ListPigPurchases godoc
|
||||||
@@ -757,14 +712,13 @@ func (c *Controller) ListPigSickLogs(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPigPurchaseRequest true "查询参数"
|
// @Param query query dto.ListPigPurchaseRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPigPurchaseResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPigPurchaseResponse}
|
||||||
// @Router /api/v1/monitor/pig-purchases [get]
|
// @Router /api/v1/monitor/pig-purchases [get]
|
||||||
func (c *Controller) ListPigPurchases(ctx *gin.Context) {
|
func (c *Controller) ListPigPurchases(ctx echo.Context) error {
|
||||||
const actionType = "获取猪只采购记录列表"
|
const actionType = "获取猪只采购记录列表"
|
||||||
|
|
||||||
var req dto.ListPigPurchaseRequest
|
var req dto.ListPigPurchaseRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PigPurchaseListOptions{
|
opts := repository.PigPurchaseListOptions{
|
||||||
@@ -780,18 +734,16 @@ func (c *Controller) ListPigPurchases(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPigSales godoc
|
// ListPigSales godoc
|
||||||
@@ -803,14 +755,13 @@ func (c *Controller) ListPigPurchases(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPigSaleRequest true "查询参数"
|
// @Param query query dto.ListPigSaleRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPigSaleResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListPigSaleResponse}
|
||||||
// @Router /api/v1/monitor/pig-sales [get]
|
// @Router /api/v1/monitor/pig-sales [get]
|
||||||
func (c *Controller) ListPigSales(ctx *gin.Context) {
|
func (c *Controller) ListPigSales(ctx echo.Context) error {
|
||||||
const actionType = "获取猪只售卖记录列表"
|
const actionType = "获取猪只售卖记录列表"
|
||||||
|
|
||||||
var req dto.ListPigSaleRequest
|
var req dto.ListPigSaleRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.PigSaleListOptions{
|
opts := repository.PigSaleListOptions{
|
||||||
@@ -826,18 +777,16 @@ func (c *Controller) ListPigSales(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListNotifications godoc
|
// ListNotifications godoc
|
||||||
@@ -849,14 +798,13 @@ func (c *Controller) ListPigSales(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListNotificationRequest true "查询参数"
|
// @Param query query dto.ListNotificationRequest true "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListNotificationResponse}
|
// @Success 200 {object} controller.Response{data=dto.ListNotificationResponse}
|
||||||
// @Router /api/v1/monitor/notifications [get]
|
// @Router /api/v1/monitor/notifications [get]
|
||||||
func (c *Controller) ListNotifications(ctx *gin.Context) {
|
func (c *Controller) ListNotifications(ctx echo.Context) error {
|
||||||
const actionType = "批量查询通知"
|
const actionType = "批量查询通知"
|
||||||
|
|
||||||
var req dto.ListNotificationRequest
|
var req dto.ListNotificationRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := repository.NotificationListOptions{
|
opts := repository.NotificationListOptions{
|
||||||
@@ -873,16 +821,14 @@ func (c *Controller) ListNotifications(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := dto.NewListNotificationResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListNotificationResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Controller 定义 ---
|
// --- 控制器定义 ---
|
||||||
|
|
||||||
// Controller 定义了计划相关的控制器
|
// Controller 定义了计划相关的控制器
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
@@ -44,21 +44,19 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal
|
|||||||
// @Param plan body dto.CreatePlanRequest true "计划信息"
|
// @Param plan body dto.CreatePlanRequest true "计划信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
|
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
|
||||||
// @Router /api/v1/plans [post]
|
// @Router /api/v1/plans [post]
|
||||||
func (c *Controller) CreatePlan(ctx *gin.Context) {
|
func (c *Controller) CreatePlan(ctx echo.Context) error {
|
||||||
var req dto.CreatePlanRequest
|
var req dto.CreatePlanRequest
|
||||||
const actionType = "创建计划"
|
const actionType = "创建计划"
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用已有的转换函数,它已经包含了验证和重排逻辑
|
// 使用已有的转换函数,它已经包含了验证和重排逻辑
|
||||||
planToCreate, err := dto.NewPlanFromCreateRequest(&req)
|
planToCreate, err := dto.NewPlanFromCreateRequest(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 业务规则处理 ---
|
// --- 业务规则处理 ---
|
||||||
@@ -76,8 +74,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
|
|||||||
// 调用仓库方法创建计划
|
// 调用仓库方法创建计划
|
||||||
if err := c.planRepo.CreatePlan(planToCreate); err != nil {
|
if err := c.planRepo.CreatePlan(planToCreate); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "数据库创建计划失败", planToCreate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "数据库创建计划失败", planToCreate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
||||||
@@ -89,14 +86,13 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
|
|||||||
// 使用已有的转换函数将创建后的模型转换为响应对象
|
// 使用已有的转换函数将创建后的模型转换为响应对象
|
||||||
resp, err := dto.NewPlanToResponse(planToCreate)
|
resp, err := dto.NewPlanToResponse(planToCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用统一的成功响应函数
|
// 使用统一的成功响应函数
|
||||||
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
|
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlan godoc
|
// GetPlan godoc
|
||||||
@@ -108,15 +104,14 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
|
|||||||
// @Param id path int true "计划ID"
|
// @Param id path int true "计划ID"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
|
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
|
||||||
// @Router /api/v1/plans/{id} [get]
|
// @Router /api/v1/plans/{id} [get]
|
||||||
func (c *Controller) GetPlan(ctx *gin.Context) {
|
func (c *Controller) GetPlan(ctx echo.Context) error {
|
||||||
const actionType = "获取计划详情"
|
const actionType = "获取计划详情"
|
||||||
// 1. 从 URL 路径中获取 ID
|
// 1. 从 URL 路径中获取 ID
|
||||||
idStr := ctx.Param("id")
|
idStr := ctx.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 调用仓库层获取计划详情
|
// 2. 调用仓库层获取计划详情
|
||||||
@@ -125,26 +120,23 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
|
|||||||
// 判断是否为“未找到”错误
|
// 判断是否为“未找到”错误
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 其他数据库错误视为内部错误
|
// 其他数据库错误视为内部错误
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 将模型转换为响应 DTO
|
// 3. 将模型转换为响应 DTO
|
||||||
resp, err := dto.NewPlanToResponse(plan)
|
resp, err := dto.NewPlanToResponse(plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送成功响应
|
// 4. 发送成功响应
|
||||||
c.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
|
c.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划详情成功", resp, actionType, "获取计划详情成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划详情成功", resp, actionType, "获取计划详情成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPlans godoc
|
// ListPlans godoc
|
||||||
@@ -156,13 +148,12 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListPlansQuery false "查询参数"
|
// @Param query query dto.ListPlansQuery false "查询参数"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListPlansResponse} "业务码为200代表成功获取列表"
|
// @Success 200 {object} controller.Response{data=dto.ListPlansResponse} "业务码为200代表成功获取列表"
|
||||||
// @Router /api/v1/plans [get]
|
// @Router /api/v1/plans [get]
|
||||||
func (c *Controller) ListPlans(ctx *gin.Context) {
|
func (c *Controller) ListPlans(ctx echo.Context) error {
|
||||||
const actionType = "获取计划列表"
|
const actionType = "获取计划列表"
|
||||||
var query dto.ListPlansQuery
|
var query dto.ListPlansQuery
|
||||||
if err := ctx.ShouldBindQuery(&query); err != nil {
|
if err := ctx.Bind(&query); err != nil {
|
||||||
c.logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 调用仓库层获取所有计划
|
// 1. 调用仓库层获取所有计划
|
||||||
@@ -170,8 +161,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
|
|||||||
plans, total, err := c.planRepo.ListPlans(opts, query.Page, query.PageSize)
|
plans, total, err := c.planRepo.ListPlans(opts, query.Page, query.PageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表时发生内部错误", actionType, "数据库查询失败", nil)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表时发生内部错误", actionType, "数据库查询失败", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 将模型转换为响应 DTO
|
// 2. 将模型转换为响应 DTO
|
||||||
@@ -180,8 +170,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
|
|||||||
resp, err := dto.NewPlanToResponse(&p)
|
resp, err := dto.NewPlanToResponse(&p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
planResponses = append(planResponses, *resp)
|
planResponses = append(planResponses, *resp)
|
||||||
}
|
}
|
||||||
@@ -192,7 +181,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
|
|||||||
Total: total,
|
Total: total,
|
||||||
}
|
}
|
||||||
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
|
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePlan godoc
|
// UpdatePlan godoc
|
||||||
@@ -206,23 +195,21 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
|
|||||||
// @Param plan body dto.UpdatePlanRequest true "更新后的计划信息"
|
// @Param plan body dto.UpdatePlanRequest true "更新后的计划信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
|
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
|
||||||
// @Router /api/v1/plans/{id} [put]
|
// @Router /api/v1/plans/{id} [put]
|
||||||
func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
func (c *Controller) UpdatePlan(ctx echo.Context) error {
|
||||||
const actionType = "更新计划"
|
const actionType = "更新计划"
|
||||||
// 1. 从 URL 路径中获取 ID
|
// 1. 从 URL 路径中获取 ID
|
||||||
idStr := ctx.Param("id")
|
idStr := ctx.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 绑定请求体
|
// 2. 绑定请求体
|
||||||
var req dto.UpdatePlanRequest
|
var req dto.UpdatePlanRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查计划是否存在
|
// 3. 检查计划是否存在
|
||||||
@@ -230,27 +217,23 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 业务规则:系统计划不允许修改
|
// 4. 业务规则:系统计划不允许修改
|
||||||
if existingPlan.PlanType == models.PlanTypeSystem {
|
if existingPlan.PlanType == models.PlanTypeSystem {
|
||||||
c.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许修改", actionType, "尝试修改系统计划", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许修改", actionType, "尝试修改系统计划", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 将请求转换为模型(转换函数带校验)
|
// 5. 将请求转换为模型(转换函数带校验)
|
||||||
planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
|
planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
planToUpdate.ID = uint(id) // 确保ID被设置
|
planToUpdate.ID = uint(id) // 确保ID被设置
|
||||||
|
|
||||||
@@ -269,8 +252,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
|||||||
|
|
||||||
if err := c.planRepo.UpdatePlan(planToUpdate); err != nil {
|
if err := c.planRepo.UpdatePlan(planToUpdate); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
|
c.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "数据库更新计划失败", planToUpdate)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "数据库更新计划失败", planToUpdate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新成功后,调用 manager 确保触发器任务定义存在
|
// 更新成功后,调用 manager 确保触发器任务定义存在
|
||||||
@@ -283,21 +265,19 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
|||||||
updatedPlan, err := c.planRepo.GetPlanByID(uint(id))
|
updatedPlan, err := c.planRepo.GetPlanByID(uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误", actionType, "获取更新后计划详情失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误", actionType, "获取更新后计划详情失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 将模型转换为响应 DTO
|
// 8. 将模型转换为响应 DTO
|
||||||
resp, err := dto.NewPlanToResponse(updatedPlan)
|
resp, err := dto.NewPlanToResponse(updatedPlan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
|
c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 发送成功响应
|
// 9. 发送成功响应
|
||||||
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
|
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePlan godoc
|
// DeletePlan godoc
|
||||||
@@ -309,15 +289,14 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
|||||||
// @Param id path int true "计划ID"
|
// @Param id path int true "计划ID"
|
||||||
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
|
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
|
||||||
// @Router /api/v1/plans/{id} [delete]
|
// @Router /api/v1/plans/{id} [delete]
|
||||||
func (c *Controller) DeletePlan(ctx *gin.Context) {
|
func (c *Controller) DeletePlan(ctx echo.Context) error {
|
||||||
const actionType = "删除计划"
|
const actionType = "删除计划"
|
||||||
// 1. 从 URL 路径中获取 ID
|
// 1. 从 URL 路径中获取 ID
|
||||||
idStr := ctx.Param("id")
|
idStr := ctx.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查计划是否存在
|
// 2. 检查计划是否存在
|
||||||
@@ -325,40 +304,35 @@ func (c *Controller) DeletePlan(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 业务规则:系统计划不允许删除
|
// 3. 业务规则:系统计划不允许删除
|
||||||
if plan.PlanType == models.PlanTypeSystem {
|
if plan.PlanType == models.PlanTypeSystem {
|
||||||
c.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许删除", actionType, "尝试删除系统计划", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许删除", actionType, "尝试删除系统计划", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 停止这个计划
|
// 4. 停止这个计划
|
||||||
if plan.Status == models.PlanStatusEnabled {
|
if plan.Status == models.PlanStatusEnabled {
|
||||||
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
||||||
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 调用仓库层删除计划
|
// 5. 调用仓库层删除计划
|
||||||
if err := c.planRepo.DeletePlan(uint(id)); err != nil {
|
if err := c.planRepo.DeletePlan(uint(id)); err != nil {
|
||||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划时发生内部错误", actionType, "数据库删除失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划时发生内部错误", actionType, "数据库删除失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 发送成功响应
|
// 6. 发送成功响应
|
||||||
c.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
|
c.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划删除成功", nil, actionType, "计划删除成功", id)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划删除成功", nil, actionType, "计划删除成功", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartPlan godoc
|
// StartPlan godoc
|
||||||
@@ -370,15 +344,14 @@ func (c *Controller) DeletePlan(ctx *gin.Context) {
|
|||||||
// @Param id path int true "计划ID"
|
// @Param id path int true "计划ID"
|
||||||
// @Success 200 {object} controller.Response "业务码为200代表成功启动计划"
|
// @Success 200 {object} controller.Response "业务码为200代表成功启动计划"
|
||||||
// @Router /api/v1/plans/{id}/start [post]
|
// @Router /api/v1/plans/{id}/start [post]
|
||||||
func (c *Controller) StartPlan(ctx *gin.Context) {
|
func (c *Controller) StartPlan(ctx echo.Context) error {
|
||||||
const actionType = "启动计划"
|
const actionType = "启动计划"
|
||||||
// 1. 从 URL 路径中获取 ID
|
// 1. 从 URL 路径中获取 ID
|
||||||
idStr := ctx.Param("id")
|
idStr := ctx.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查计划是否存在
|
// 2. 检查计划是否存在
|
||||||
@@ -386,24 +359,20 @@ func (c *Controller) StartPlan(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 业务规则检查
|
// 3. 业务规则检查
|
||||||
if plan.PlanType == models.PlanTypeSystem {
|
if plan.PlanType == models.PlanTypeSystem {
|
||||||
c.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许手动启动", actionType, "尝试手动启动系统计划", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许手动启动", actionType, "尝试手动启动系统计划", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if plan.Status == models.PlanStatusEnabled {
|
if plan.Status == models.PlanStatusEnabled {
|
||||||
c.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 检查并重置执行计数器,然后更新计划状态为“已启动”
|
// 4. 检查并重置执行计数器,然后更新计划状态为“已启动”
|
||||||
@@ -413,8 +382,7 @@ func (c *Controller) StartPlan(ctx *gin.Context) {
|
|||||||
if plan.ExecuteCount > 0 {
|
if plan.ExecuteCount > 0 {
|
||||||
if err := c.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
|
if err := c.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
|
||||||
c.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
|
c.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "重置计划执行计数失败", actionType, "重置执行计数失败", plan.ID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "重置计划执行计数失败", actionType, "重置执行计数失败", plan.ID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
|
c.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
|
||||||
}
|
}
|
||||||
@@ -422,28 +390,25 @@ func (c *Controller) StartPlan(ctx *gin.Context) {
|
|||||||
// 更新计划状态为“已启动”
|
// 更新计划状态为“已启动”
|
||||||
if err := c.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
|
if err := c.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
|
||||||
c.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
|
c.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划状态失败", actionType, "更新计划状态失败", plan.ID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划状态失败", actionType, "更新计划状态失败", plan.ID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
|
c.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
|
||||||
} else {
|
} else {
|
||||||
// 如果计划已经处于 Enabled 状态,则无需更新
|
// 如果计划已经处于 Enabled 状态,则无需更新
|
||||||
c.logger.Infof("计划 #%d 已处于启动状态,无需重复操作。", plan.ID)
|
c.logger.Infof("计划 #%d 已处于启动状态,无需重复操作。", plan.ID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", plan.ID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", plan.ID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 为计划创建或更新触发器
|
// 5. 为计划创建或更新触发器
|
||||||
if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
|
if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
|
||||||
// 此处错误不回滚状态,因为状态更新已成功,但需要明确告知用户触发器创建失败
|
// 此处错误不回滚状态,因为状态更新已成功,但需要明确告知用户触发器创建失败
|
||||||
c.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
|
c.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划状态已更新,但创建执行触发器失败,请检查计划配置或稍后重试", actionType, "创建执行触发器失败", plan.ID)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划状态已更新,但创建执行触发器失败,请检查计划配置或稍后重试", actionType, "创建执行触发器失败", plan.ID)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 发送成功响应
|
// 6. 发送成功响应
|
||||||
c.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
|
c.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功启动", nil, actionType, "计划已成功启动", id)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功启动", nil, actionType, "计划已成功启动", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopPlan godoc
|
// StopPlan godoc
|
||||||
@@ -455,15 +420,14 @@ func (c *Controller) StartPlan(ctx *gin.Context) {
|
|||||||
// @Param id path int true "计划ID"
|
// @Param id path int true "计划ID"
|
||||||
// @Success 200 {object} controller.Response "业务码为200代表成功停止计划"
|
// @Success 200 {object} controller.Response "业务码为200代表成功停止计划"
|
||||||
// @Router /api/v1/plans/{id}/stop [post]
|
// @Router /api/v1/plans/{id}/stop [post]
|
||||||
func (c *Controller) StopPlan(ctx *gin.Context) {
|
func (c *Controller) StopPlan(ctx echo.Context) error {
|
||||||
const actionType = "停止计划"
|
const actionType = "停止计划"
|
||||||
// 1. 从 URL 路径中获取 ID
|
// 1. 从 URL 路径中获取 ID
|
||||||
idStr := ctx.Param("id")
|
idStr := ctx.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查计划是否存在
|
// 2. 检查计划是否存在
|
||||||
@@ -471,36 +435,31 @@ func (c *Controller) StopPlan(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 业务规则:系统计划不允许停止
|
// 3. 业务规则:系统计划不允许停止
|
||||||
if plan.PlanType == models.PlanTypeSystem {
|
if plan.PlanType == models.PlanTypeSystem {
|
||||||
c.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
|
c.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许停止", actionType, "尝试停止系统计划", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许停止", actionType, "尝试停止系统计划", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 检查计划当前状态
|
// 4. 检查计划当前状态
|
||||||
if plan.Status != models.PlanStatusEnabled {
|
if plan.Status != models.PlanStatusEnabled {
|
||||||
c.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
|
c.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划当前不是启用状态", actionType, "计划未启用", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划当前不是启用状态", actionType, "计划未启用", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 调用仓库层方法,该方法内部处理事务
|
// 5. 调用仓库层方法,该方法内部处理事务
|
||||||
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
||||||
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 发送成功响应
|
// 6. 发送成功响应
|
||||||
c.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
|
c.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功停止", nil, actionType, "计划已成功停止", id)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功停止", nil, actionType, "计划已成功停止", id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,827 +0,0 @@
|
|||||||
package plan
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试
|
|
||||||
type MockPlanRepository struct {
|
|
||||||
// CreatePlanFunc 模拟 CreatePlan 方法的行为
|
|
||||||
CreatePlanFunc func(plan *models.Plan) error
|
|
||||||
// GetPlanByIDFunc 模拟 GetPlanByID 方法的行为
|
|
||||||
GetPlanByIDFunc func(id uint) (*models.Plan, error)
|
|
||||||
// GetBasicPlanByIDFunc 模拟 GetBasicPlanByID 方法的行为
|
|
||||||
GetBasicPlanByIDFunc func(id uint) (*models.Plan, error)
|
|
||||||
// ListBasicPlansFunc 模拟 ListBasicPlans 方法的行为
|
|
||||||
ListBasicPlansFunc func() ([]models.Plan, error)
|
|
||||||
// UpdatePlanFunc 模拟 UpdatePlan 方法的行为
|
|
||||||
UpdatePlanFunc func(plan *models.Plan) error
|
|
||||||
// DeletePlanFunc 模拟 DeletePlan 方法的行为
|
|
||||||
DeletePlanFunc func(id uint) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBasicPlans 实现了 MockPlanRepository 接口的 ListBasicPlans 方法
|
|
||||||
func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) {
|
|
||||||
return m.ListBasicPlansFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBasicPlanByID 实现了 MockPlanRepository 接口的 GetBasicPlanByID 方法
|
|
||||||
func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
|
|
||||||
return m.GetBasicPlanByIDFunc(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPlanByID 实现了 MockPlanRepository 接口的 GetPlanByID 方法
|
|
||||||
func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
|
|
||||||
return m.GetPlanByIDFunc(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePlan 实现了 MockPlanRepository 接口的 CreatePlan 方法
|
|
||||||
func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error {
|
|
||||||
return m.CreatePlanFunc(plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePlan 实现了 MockPlanRepository 接口的 UpdatePlan 方法
|
|
||||||
func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error {
|
|
||||||
return m.UpdatePlanFunc(plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePlan 实现了 MockPlanRepository 接口的 DeletePlan 方法
|
|
||||||
func (m *MockPlanRepository) DeletePlan(id uint) error {
|
|
||||||
return m.DeletePlanFunc(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例
|
|
||||||
func setupTestRouter(repo repository.PlanRepository) *gin.Engine {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.Default()
|
|
||||||
logger := logs.NewSilentLogger()
|
|
||||||
planController := NewController(logger, repo)
|
|
||||||
router.POST("/plans", planController.CreatePlan)
|
|
||||||
router.GET("/plans/:id", planController.GetPlan)
|
|
||||||
router.GET("/plans", planController.ListPlans)
|
|
||||||
router.PUT("/plans/:id", planController.UpdatePlan)
|
|
||||||
router.DELETE("/plans/:id", planController.DeletePlan)
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestController_CreatePlan 测试 CreatePlan 方法
|
|
||||||
func TestController_CreatePlan(t *testing.T) {
|
|
||||||
t.Run("成功-创建包含任务的计划", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:CreatePlan 成功时,为计划和任务分配ID
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
CreatePlanFunc: func(plan *models.Plan) error {
|
|
||||||
plan.ID = 1
|
|
||||||
for i := range plan.Tasks {
|
|
||||||
plan.Tasks[i].ID = uint(i + 1)
|
|
||||||
plan.Tasks[i].PlanID = plan.ID
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// 设置 Gin 路由器,并注入模拟仓库
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备请求体
|
|
||||||
reqBody := CreatePlanRequest{
|
|
||||||
Name: "Test Plan with Tasks",
|
|
||||||
ExecutionType: models.PlanExecutionTypeManual,
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
Tasks: []TaskRequest{
|
|
||||||
{Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
// 发送 HTTP 请求到路由器
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
// 验证 HTTP 状态码
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
// 解析响应体
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// 验证业务响应码和消息
|
|
||||||
assert.Equal(t, controller.CodeCreated, resp.Code)
|
|
||||||
assert.Equal(t, "计划创建成功", resp.Message)
|
|
||||||
|
|
||||||
// 验证返回数据中的计划ID
|
|
||||||
dataMap, ok := resp.Data.(map[string]interface{})
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, float64(1), dataMap["id"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数
|
|
||||||
func TestController_GetPlan(t *testing.T) {
|
|
||||||
t.Run("成功-获取计划详情", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:GetPlanByID 成功时返回一个计划
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
assert.Equal(t, uint(1), id)
|
|
||||||
return &models.Plan{
|
|
||||||
Model: gorm.Model{ID: 1},
|
|
||||||
Name: "Test Plan",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// 设置 Gin 路由器
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
// 创建 HTTP 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
dataMap, ok := resp.Data.(map[string]interface{})
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, float64(1), dataMap["id"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("成功-获取内容为空的计划详情", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:GetPlanByID 成功时返回一个任务列表为空的计划
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
assert.Equal(t, uint(3), id)
|
|
||||||
return &models.Plan{
|
|
||||||
Model: gorm.Model{ID: 3},
|
|
||||||
Name: "Empty Plan",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
Tasks: []models.Task{}, // 任务列表为空
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans/3", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
|
|
||||||
dataMap, ok := resp.Data.(map[string]interface{})
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, float64(3), dataMap["id"])
|
|
||||||
assert.Equal(t, "Empty Plan", dataMap["name"])
|
|
||||||
|
|
||||||
// 关键断言:因为 omitempty 标签,当 tasks 列表为空时,该字段不应该出现在JSON中
|
|
||||||
_, ok = dataMap["tasks"]
|
|
||||||
assert.False(t, ok, "当任务列表为空时,'tasks' 字段因为 omitempty 标签,不应该出现在JSON响应中")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:GetPlanByID 返回记录未找到错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return nil, gorm.ErrRecordNotFound
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans/999", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeNotFound, resp.Code)
|
|
||||||
assert.Equal(t, "计划不存在", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
|
||||||
mockRepo := &MockPlanRepository{}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
// 创建带有无效ID格式的 HTTP 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans/abc", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-仓库层内部错误", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
internalErr := errors.New("database connection lost")
|
|
||||||
// 模拟仓库行为:GetPlanByID 返回内部错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return nil, internalErr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
|
||||||
assert.Equal(t, "获取计划详情时发生内部错误", resp.Message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestController_ListPlans 测试 ListPlans 方法
|
|
||||||
func TestController_ListPlans(t *testing.T) {
|
|
||||||
t.Run("成功-获取计划列表", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟返回的计划列表
|
|
||||||
mockPlans := []models.Plan{
|
|
||||||
{Model: gorm.Model{ID: 1}, Name: "Plan 1", ContentType: models.PlanContentTypeTasks},
|
|
||||||
{Model: gorm.Model{ID: 2}, Name: "Plan 2", ContentType: models.PlanContentTypeTasks},
|
|
||||||
}
|
|
||||||
// 模拟仓库行为:ListBasicPlans 成功时返回计划列表
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
|
||||||
return mockPlans, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
assert.Equal(t, "获取计划列表成功", resp.Message)
|
|
||||||
|
|
||||||
dataBytes, err := json.Marshal(resp.Data)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var listResp ListPlansResponse
|
|
||||||
err = json.Unmarshal(dataBytes, &listResp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, 2, listResp.Total)
|
|
||||||
assert.Len(t, listResp.Plans, 2)
|
|
||||||
assert.Equal(t, uint(1), listResp.Plans[0].ID)
|
|
||||||
assert.Equal(t, "Plan 1", listResp.Plans[0].Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("成功-返回空列表", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:ListBasicPlans 返回空列表
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
|
||||||
return []models.Plan{}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
|
|
||||||
dataBytes, err := json.Marshal(resp.Data)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var listResp ListPlansResponse
|
|
||||||
err = json.Unmarshal(dataBytes, &listResp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, 0, listResp.Total)
|
|
||||||
assert.Len(t, listResp.Plans, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-仓库层返回错误", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
dbErr := errors.New("db error")
|
|
||||||
// 模拟仓库行为:ListBasicPlans 返回数据库错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
|
||||||
return nil, dbErr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
|
||||||
assert.Equal(t, "获取计划列表时发生内部错误", resp.Message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestController_UpdatePlan 是 UpdatePlan 的测试函数
|
|
||||||
func TestController_UpdatePlan(t *testing.T) {
|
|
||||||
t.Run("成功-更新计划", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(1)
|
|
||||||
updatedName := "Updated Plan Name"
|
|
||||||
// 模拟一个已存在的计划
|
|
||||||
mockPlan := &models.Plan{
|
|
||||||
Model: gorm.Model{ID: planID},
|
|
||||||
Name: "Original Plan",
|
|
||||||
Description: "Original Description",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
// 配置模拟仓库的行为
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
|
||||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
assert.Equal(t, planID, id)
|
|
||||||
return mockPlan, nil
|
|
||||||
},
|
|
||||||
// 模拟 UpdatePlan 成功更新计划,并更新 mockPlan 的名称
|
|
||||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
|
||||||
assert.Equal(t, planID, plan.ID)
|
|
||||||
assert.Equal(t, updatedName, plan.Name)
|
|
||||||
mockPlan.Name = plan.Name // 模拟更新操作
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
// 模拟 GetPlanByID 返回更新后的计划
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
assert.Equal(t, planID, id)
|
|
||||||
return mockPlan, nil // 返回已更新的 mockPlan
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// 设置 Gin 路由器,并注入模拟仓库
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备更新请求体
|
|
||||||
reqBody := UpdatePlanRequest{
|
|
||||||
Name: updatedName,
|
|
||||||
Description: "Updated Description",
|
|
||||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
// 发送 HTTP 请求到路由器
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
// 验证 HTTP 状态码
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
// 解析响应体
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// 验证业务响应码、消息和返回数据
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
assert.Equal(t, "计划更新成功", resp.Message)
|
|
||||||
|
|
||||||
dataMap, ok := resp.Data.(map[string]interface{})
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, float64(planID), dataMap["id"])
|
|
||||||
assert.Equal(t, updatedName, dataMap["name"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
|
||||||
mockRepo := &MockPlanRepository{}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
// 创建带有无效ID格式的 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/abc", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-请求体绑定失败", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(1)
|
|
||||||
// 模拟仓库为空,因为预期不会调用仓库方法(请求体绑定失败发生在控制器内部)
|
|
||||||
mockRepo := &MockPlanRepository{}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备一个无效的 JSON 请求体,例如 execution_type 类型错误
|
|
||||||
reqBody := `{\"name\": \"Updated Plan Name\",}`
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBufferString(reqBody))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Contains(t, resp.Message, "无效的请求体")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(999)
|
|
||||||
// 模拟仓库行为:GetBasicPlanByID 返回记录未找到错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
assert.Equal(t, planID, id)
|
|
||||||
return nil, gorm.ErrRecordNotFound
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备有效的请求体
|
|
||||||
reqBody := UpdatePlanRequest{
|
|
||||||
Name: "Updated Plan Name",
|
|
||||||
Description: "Updated Description",
|
|
||||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeNotFound, resp.Code)
|
|
||||||
assert.Equal(t, "计划不存在", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-计划数据校验失败", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(1)
|
|
||||||
// 模拟一个已存在的计划
|
|
||||||
mockPlan := &models.Plan{
|
|
||||||
Model: gorm.Model{ID: planID},
|
|
||||||
Name: "Original Plan",
|
|
||||||
Description: "Original Description",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
// 配置模拟仓库行为:GetBasicPlanByID 成功返回现有计划
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return mockPlan, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备一个会导致 PlanFromUpdateRequest 校验失败的请求体。
|
|
||||||
// 这里通过提供重复的 ExecutionOrder 来触发 ValidateExecutionOrder 错误。
|
|
||||||
reqBody := UpdatePlanRequest{
|
|
||||||
Name: "Invalid Plan",
|
|
||||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
|
||||||
ContentType: models.PlanContentTypeTasks, // 设置为任务类型
|
|
||||||
Tasks: []TaskRequest{
|
|
||||||
{Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting},
|
|
||||||
{Name: "Task 2", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, // 重复的执行顺序
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Contains(t, resp.Message, "计划数据校验失败")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-仓库层更新失败", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(1)
|
|
||||||
// 模拟一个已存在的计划
|
|
||||||
mockPlan := &models.Plan{
|
|
||||||
Model: gorm.Model{ID: planID},
|
|
||||||
Name: "Original Plan",
|
|
||||||
Description: "Original Description",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
updateErr := errors.New("failed to update in repository")
|
|
||||||
// 配置模拟仓库行为
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
|
||||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return mockPlan, nil
|
|
||||||
},
|
|
||||||
// 模拟 UpdatePlan 返回更新失败错误
|
|
||||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
|
||||||
return updateErr // 模拟更新失败
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备有效的请求体
|
|
||||||
reqBody := UpdatePlanRequest{
|
|
||||||
Name: "Updated Plan Name",
|
|
||||||
Description: "Updated Description",
|
|
||||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Equal(t, "更新计划失败: "+updateErr.Error(), resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-获取更新后计划失败", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
planID := uint(1)
|
|
||||||
// 模拟一个已存在的计划
|
|
||||||
mockPlan := &models.Plan{
|
|
||||||
Model: gorm.Model{ID: planID},
|
|
||||||
Name: "Original Plan",
|
|
||||||
Description: "Original Description",
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
getUpdatedErr := errors.New("failed to get updated plan from repository")
|
|
||||||
// 配置模拟仓库行为
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
|
||||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return mockPlan, nil
|
|
||||||
},
|
|
||||||
// 模拟 UpdatePlan 成功
|
|
||||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
|
||||||
return nil // 模拟成功更新
|
|
||||||
},
|
|
||||||
// 模拟 GetPlanByID 返回获取失败错误
|
|
||||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
|
||||||
return nil, getUpdatedErr // 模拟获取更新后计划失败
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
|
|
||||||
// 准备有效的请求体
|
|
||||||
reqBody := UpdatePlanRequest{
|
|
||||||
Name: "Updated Plan Name",
|
|
||||||
Description: "Updated Description",
|
|
||||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
|
||||||
ContentType: models.PlanContentTypeTasks,
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// 创建 HTTP PUT 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
|
||||||
assert.Equal(t, "获取更新后计划详情时发生内部错误", resp.Message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestController_DeletePlan 是 DeletePlan 的单元测试
|
|
||||||
func TestController_DeletePlan(t *testing.T) {
|
|
||||||
t.Run("成功-删除计划", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:DeletePlan 成功
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
DeletePlanFunc: func(id uint) error {
|
|
||||||
assert.Equal(t, uint(1), id)
|
|
||||||
return nil // 模拟成功删除
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
|
||||||
assert.Equal(t, "计划删除成功", resp.Message)
|
|
||||||
assert.Nil(t, resp.Data)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库行为:DeletePlan 返回记录未找到错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
DeletePlanFunc: func(id uint) error {
|
|
||||||
return gorm.ErrRecordNotFound // 模拟未找到记录
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/999", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
|
||||||
assert.Equal(t, "删除计划时发生内部错误", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
|
||||||
mockRepo := &MockPlanRepository{}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
// 创建带有无效ID格式的 HTTP DELETE 请求
|
|
||||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/abc", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
|
||||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("失败-仓库层内部错误", func(t *testing.T) {
|
|
||||||
// Arrange (准备阶段)
|
|
||||||
internalErr := errors.New("something went wrong")
|
|
||||||
// 模拟仓库行为:DeletePlan 返回内部错误
|
|
||||||
mockRepo := &MockPlanRepository{
|
|
||||||
|
|
||||||
DeletePlanFunc: func(id uint) error {
|
|
||||||
return internalErr // 模拟内部错误
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := setupTestRouter(mockRepo)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil)
|
|
||||||
|
|
||||||
// Act (执行阶段)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
// Assert (断言阶段)
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var resp controller.Response
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
|
||||||
assert.Equal(t, "删除计划时发生内部错误", resp.Message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- 业务状态码 ---
|
// --- 业务状态码 ---
|
||||||
@@ -33,12 +33,13 @@ const (
|
|||||||
type Response struct {
|
type Response struct {
|
||||||
Code ResponseCode `json:"code"` // 业务状态码
|
Code ResponseCode `json:"code"` // 业务状态码
|
||||||
Message string `json:"message"` // 提示信息
|
Message string `json:"message"` // 提示信息
|
||||||
Data interface{} `json:"data"` // 业务数据
|
Data interface{} `json:"data,omitempty"` // 业务数据, omitempty表示如果为空则不序列化
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendResponse 发送统一格式的JSON响应 (基础函数,不带审计)
|
// SendResponse 发送统一格式的JSON响应 (基础函数,不带审计)
|
||||||
func SendResponse(ctx *gin.Context, code ResponseCode, message string, data interface{}) {
|
// 所有的业务API都应该使用这个函数返回,以确保HTTP状态码始终为200 OK。
|
||||||
ctx.JSON(http.StatusOK, Response{
|
func SendResponse(c echo.Context, code ResponseCode, message string, data interface{}) error {
|
||||||
|
return c.JSON(http.StatusOK, Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: message,
|
Message: message,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -46,51 +47,63 @@ func SendResponse(ctx *gin.Context, code ResponseCode, message string, data inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendErrorResponse 发送统一格式的错误响应 (基础函数,不带审计)
|
// SendErrorResponse 发送统一格式的错误响应 (基础函数,不带审计)
|
||||||
func SendErrorResponse(ctx *gin.Context, code ResponseCode, message string) {
|
// HTTP状态码为200 OK,通过业务码表示错误。
|
||||||
SendResponse(ctx, code, message, nil)
|
func SendErrorResponse(c echo.Context, code ResponseCode, message string) error {
|
||||||
|
return SendResponse(c, code, message, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendErrorWithStatus 发送带有指定HTTP状态码的错误响应。
|
||||||
|
// 这个函数主要用于中间件或特殊场景(如认证失败),在这些场景下需要返回非200的HTTP状态码。
|
||||||
|
func SendErrorWithStatus(c echo.Context, httpStatus int, code ResponseCode, message string) error {
|
||||||
|
return c.JSON(httpStatus, Response{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 带审计功能的响应函数 ---
|
// --- 带审计功能的响应函数 ---
|
||||||
|
|
||||||
// setAuditDetails 是一个内部辅助函数,用于在 gin.Context 中设置业务相关的审计信息。
|
// setAuditDetails 是一个内部辅助函数,用于在 echo.Context 中统一设置所有业务相关的审计信息。
|
||||||
func setAuditDetails(c *gin.Context, actionType, description string, targetResource interface{}) {
|
func setAuditDetails(c echo.Context, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string) {
|
||||||
// 只有当 actionType 不为空时,才设置审计信息,这作为触发审计的标志
|
// 只有当 actionType 不为空时,才设置审计信息,这作为触发审计的标志
|
||||||
if actionType != "" {
|
if actionType != "" {
|
||||||
c.Set(models.ContextAuditActionType.String(), actionType)
|
c.Set(models.ContextAuditActionType.String(), actionType)
|
||||||
c.Set(models.ContextAuditDescription.String(), description)
|
c.Set(models.ContextAuditDescription.String(), description)
|
||||||
c.Set(models.ContextAuditTargetResource.String(), targetResource)
|
c.Set(models.ContextAuditTargetResource.String(), targetResource)
|
||||||
|
c.Set(models.ContextAuditStatus.String(), status)
|
||||||
|
c.Set(models.ContextAuditResultDetails.String(), resultDetails)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendSuccessWithAudit 发送成功的响应,并设置审计日志所需的信息。
|
// SendSuccessWithAudit 发送成功的响应,并设置审计日志所需的信息。
|
||||||
// 这是控制器中用于记录成功操作并返回响应的首选函数。
|
// 这是控制器中用于记录成功操作并返回响应的首选函数。
|
||||||
func SendSuccessWithAudit(
|
func SendSuccessWithAudit(
|
||||||
ctx *gin.Context, // Gin上下文,用于处理HTTP请求和响应
|
c echo.Context, // Echo上下文,用于处理HTTP请求和响应
|
||||||
code ResponseCode, // 业务状态码,表示操作结果
|
code ResponseCode, // 业务状态码,表示操作结果
|
||||||
message string, // 提示信息,向用户展示操作结果的文本描述
|
message string, // 提示信息,向用户展示操作结果的文本描述
|
||||||
data interface{}, // 业务数据,操作成功后返回的具体数据
|
data interface{}, // 业务数据,操作成功后返回的具体数据
|
||||||
actionType string, // 审计操作类型,例如"创建用户", "更新配置"
|
actionType string, // 审计操作类型,例如"创建用户", "更新配置"
|
||||||
description string, // 审计描述,对操作的详细说明
|
description string, // 审计描述,对操作的详细说明
|
||||||
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
||||||
) {
|
) error {
|
||||||
// 1. 设置审计信息
|
// 1. 设置审计信息
|
||||||
setAuditDetails(ctx, actionType, description, targetResource)
|
setAuditDetails(c, actionType, description, targetResource, models.AuditStatusSuccess, "")
|
||||||
// 2. 发送响应
|
// 2. 发送响应
|
||||||
SendResponse(ctx, code, message, data)
|
return SendResponse(c, code, message, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendErrorWithAudit 发送失败的响应,并设置审计日志所需的信息。
|
// SendErrorWithAudit 发送失败的响应,并设置审计日志所需的信息。
|
||||||
// 这是控制器中用于记录失败操作并返回响应的首选函数。
|
// 这是控制器中用于记录失败操作并返回响应的首选函数。
|
||||||
func SendErrorWithAudit(
|
func SendErrorWithAudit(
|
||||||
ctx *gin.Context, // Gin上下文,用于处理HTTP请求和响应
|
c echo.Context, // Echo上下文,用于处理HTTP请求和响应
|
||||||
code ResponseCode, // 业务状态码,表示操作结果
|
code ResponseCode, // 业务状态码,表示操作结果
|
||||||
message string, // 提示信息,向用户展示操作结果的文本描述
|
message string, // 提示信息,向用户展示操作结果的文本描述
|
||||||
actionType string, // 审计操作类型,例如"登录失败", "删除失败"
|
actionType string, // 审计操作类型,例如"登录失败", "删除失败"
|
||||||
description string, // 审计描述,对操作的详细说明
|
description string, // 审计描述,对操作的详细说明
|
||||||
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
||||||
) {
|
) error {
|
||||||
// 1. 设置审计信息
|
// 1. 设置审计信息
|
||||||
setAuditDetails(ctx, actionType, description, targetResource)
|
setAuditDetails(c, actionType, description, targetResource, models.AuditStatusFailed, message)
|
||||||
// 2. 发送响应
|
// 2. 发送响应
|
||||||
SendErrorResponse(ctx, code, message)
|
return SendErrorResponse(c, code, message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,12 +53,11 @@ func NewController(
|
|||||||
// @Param user body dto.CreateUserRequest true "用户信息"
|
// @Param user body dto.CreateUserRequest true "用户信息"
|
||||||
// @Success 200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
|
// @Success 200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
|
||||||
// @Router /api/v1/users [post]
|
// @Router /api/v1/users [post]
|
||||||
func (c *Controller) CreateUser(ctx *gin.Context) {
|
func (c *Controller) CreateUser(ctx echo.Context) error {
|
||||||
var req dto.CreateUserRequest
|
var req dto.CreateUserRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
|
c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
|
||||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &models.User{
|
user := &models.User{
|
||||||
@@ -72,16 +71,14 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
|
|||||||
// 尝试查询用户,以判断是否是用户名重复导致的错误
|
// 尝试查询用户,以判断是否是用户名重复导致的错误
|
||||||
_, findErr := c.userRepo.FindByUsername(req.Username)
|
_, findErr := c.userRepo.FindByUsername(req.Username)
|
||||||
if findErr == nil { // 如果能找到用户,说明是用户名重复
|
if findErr == nil { // 如果能找到用户,说明是用户名重复
|
||||||
controller.SendErrorResponse(ctx, controller.CodeConflict, "用户名已存在")
|
return controller.SendErrorResponse(ctx, controller.CodeConflict, "用户名已存在")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他创建失败的情况
|
// 其他创建失败的情况
|
||||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "创建用户失败")
|
return controller.SendErrorResponse(ctx, controller.CodeInternalError, "创建用户失败")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{
|
return controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
})
|
})
|
||||||
@@ -96,40 +93,35 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
|
|||||||
// @Param credentials body dto.LoginRequest true "登录凭证"
|
// @Param credentials body dto.LoginRequest true "登录凭证"
|
||||||
// @Success 200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
|
// @Success 200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
|
||||||
// @Router /api/v1/users/login [post]
|
// @Router /api/v1/users/login [post]
|
||||||
func (c *Controller) Login(ctx *gin.Context) {
|
func (c *Controller) Login(ctx echo.Context) error {
|
||||||
var req dto.LoginRequest
|
var req dto.LoginRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("登录: 参数绑定失败: %v", err)
|
c.logger.Errorf("登录: 参数绑定失败: %v", err)
|
||||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户
|
// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户
|
||||||
user, err := c.userRepo.FindUserForLogin(req.Identifier)
|
user, err := c.userRepo.FindUserForLogin(req.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("登录: 查询用户失败: %v", err)
|
c.logger.Errorf("登录: 查询用户失败: %v", err)
|
||||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败")
|
return controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.CheckPassword(req.Password) {
|
if !user.CheckPassword(req.Password) {
|
||||||
controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功,生成 JWT token
|
// 登录成功,生成 JWT token
|
||||||
tokenString, err := c.tokenService.GenerateToken(user.ID)
|
tokenString, err := c.tokenService.GenerateToken(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("登录: 生成令牌失败: %v", err)
|
c.logger.Errorf("登录: 生成令牌失败: %v", err)
|
||||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败,无法生成认证信息")
|
return controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败,无法生成认证信息")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{
|
return controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Token: tokenString,
|
Token: tokenString,
|
||||||
@@ -146,7 +138,7 @@ func (c *Controller) Login(ctx *gin.Context) {
|
|||||||
// @Param query query dto.ListUserActionLogRequest false "查询参数 (除了 user_id,它被路径中的ID覆盖)"
|
// @Param query query dto.ListUserActionLogRequest false "查询参数 (除了 user_id,它被路径中的ID覆盖)"
|
||||||
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse} "业务码为200代表成功获取"
|
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse} "业务码为200代表成功获取"
|
||||||
// @Router /api/v1/users/{id}/history [get]
|
// @Router /api/v1/users/{id}/history [get]
|
||||||
func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
func (c *Controller) ListUserHistory(ctx echo.Context) error {
|
||||||
const actionType = "获取用户操作历史"
|
const actionType = "获取用户操作历史"
|
||||||
|
|
||||||
// 1. 解析路径中的用户ID,它的优先级最高
|
// 1. 解析路径中的用户ID,它的优先级最高
|
||||||
@@ -154,16 +146,14 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
|||||||
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 无效的用户ID格式: %v, ID: %s", actionType, err, userIDStr)
|
c.logger.Errorf("%s: 无效的用户ID格式: %v, ID: %s", actionType, err, userIDStr)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", userIDStr)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", userIDStr)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 绑定通用的查询请求 DTO
|
// 2. 绑定通用的查询请求 DTO
|
||||||
var req dto.ListUserActionLogRequest
|
var req dto.ListUserActionLogRequest
|
||||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 准备 Service 调用参数,并强制使用路径中的 UserID
|
// 3. 准备 Service 调用参数,并强制使用路径中的 UserID
|
||||||
@@ -188,18 +178,16 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", opts)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", opts)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户历史记录失败", actionType, "服务层查询失败", opts)
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户历史记录失败", actionType, "服务层查询失败", opts)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 使用复用的 DTO 构建并发送成功响应
|
// 5. 使用复用的 DTO 构建并发送成功响应
|
||||||
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
||||||
c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(data))
|
c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(data))
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", opts)
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTestNotification godoc
|
// SendTestNotification godoc
|
||||||
@@ -213,34 +201,31 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
|||||||
// @Param body body dto.SendTestNotificationRequest true "请求体"
|
// @Param body body dto.SendTestNotificationRequest true "请求体"
|
||||||
// @Success 200 {object} controller.Response{data=string} "成功响应"
|
// @Success 200 {object} controller.Response{data=string} "成功响应"
|
||||||
// @Router /api/v1/users/{id}/notifications/test [post]
|
// @Router /api/v1/users/{id}/notifications/test [post]
|
||||||
func (c *Controller) SendTestNotification(ctx *gin.Context) {
|
func (c *Controller) SendTestNotification(ctx echo.Context) error {
|
||||||
const actionType = "发送测试通知"
|
const actionType = "发送测试通知"
|
||||||
|
|
||||||
// 1. 从 URL 中获取用户 ID
|
// 1. 从 URL 中获取用户 ID
|
||||||
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
|
c.logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 从请求体 (JSON Body) 中获取要测试的通知类型
|
// 2. 从请求体 (JSON Body) 中获取要测试的通知类型
|
||||||
var req dto.SendTestNotificationRequest
|
var req dto.SendTestNotificationRequest
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
if err := ctx.Bind(&req); err != nil {
|
||||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req)
|
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 调用领域服务
|
// 3. 调用领域服务
|
||||||
err = c.notifyService.SendTestMessage(uint(userID), req.Type)
|
err = c.notifyService.SendTestMessage(uint(userID), req.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
|
c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
|
||||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", gin.H{"userID": userID, "type": req.Type})
|
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 返回成功响应
|
// 4. 返回成功响应
|
||||||
c.logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type)
|
c.logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type)
|
||||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", gin.H{"userID": userID, "type": req.Type})
|
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", map[string]interface{}{"userID": userID, "type": req.Type})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,450 +0,0 @@
|
|||||||
package user_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockUserRepository 是 UserRepository 接口的模拟实现
|
|
||||||
type MockUserRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTx 模拟 UserRepository 的 CreateTx 方法
|
|
||||||
func (m *MockUserRepository) Create(user *models.User) error {
|
|
||||||
args := m.Called(user)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindByUsername 模拟 UserRepository 的 FindByUsername 方法
|
|
||||||
// 返回类型改回 *models.User
|
|
||||||
func (m *MockUserRepository) FindByUsername(username string) (*models.User, error) {
|
|
||||||
args := m.Called(username)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).(*models.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindByID 模拟 UserRepository 的 FindByID 方法
|
|
||||||
func (m *MockUserRepository) FindByID(id uint) (*models.User, error) {
|
|
||||||
args := m.Called(id)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).(*models.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockTokenService 是 token.TokenService 接口的模拟实现
|
|
||||||
type MockTokenService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateToken 模拟 TokenService 的 GenerateToken 方法
|
|
||||||
func (m *MockTokenService) GenerateToken(userID uint) (string, error) {
|
|
||||||
args := m.Called(userID)
|
|
||||||
return args.String(0), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseToken 模拟 TokenService 的 ParseToken 方法
|
|
||||||
func (m *MockTokenService) ParseToken(tokenString string) (*token.Claims, error) {
|
|
||||||
args := m.Called(tokenString)
|
|
||||||
if args.Get(0) == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
return args.Get(0).(*token.Claims), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCreateUser 测试 CreateUser 方法
|
|
||||||
func TestCreateUser(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode) // 设置 Gin 为测试模式
|
|
||||||
|
|
||||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
|
||||||
silentLogger := logs.NewSilentLogger()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody user.CreateUserRequest
|
|
||||||
mockRepoSetup func(*MockUserRepository)
|
|
||||||
expectedResponse map[string]interface{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "成功创建用户",
|
|
||||||
requestBody: user.CreateUserRequest{
|
|
||||||
Username: "testuser",
|
|
||||||
Password: "password123",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
// 模拟 CreateTx 成功
|
|
||||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
|
|
||||||
// 模拟数据库自动填充 ID
|
|
||||||
userArg := args.Get(0).(*models.User)
|
|
||||||
userArg.ID = 1 // 设置一个非零的 ID
|
|
||||||
}).Once()
|
|
||||||
// 在成功创建用户的路径下,FindByUsername 不会被调用,因此这里不需要设置其期望
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeCreated), // 修改这里:使用自定义状态码
|
|
||||||
"message": "用户创建成功",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"username": "testuser",
|
|
||||||
// "id": mock.Anything, // 移除这里的 id,在断言时单独检查
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败_密码过短",
|
|
||||||
requestBody: user.CreateUserRequest{
|
|
||||||
Username: "testuser2",
|
|
||||||
Password: "123", // 密码少于6位
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
// 不会调用 CreateTx 或 FindByUsername
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeBadRequest),
|
|
||||||
"message": "Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败_缺少用户名",
|
|
||||||
requestBody: user.CreateUserRequest{
|
|
||||||
Password: "password123",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
// 不会调用 CreateTx 或 FindByUsername
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeBadRequest),
|
|
||||||
"message": "Key: 'CreateUserRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户名已存在",
|
|
||||||
requestBody: user.CreateUserRequest{
|
|
||||||
Username: "existinguser",
|
|
||||||
Password: "password123",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
// 模拟 CreateTx 失败,因为用户名已存在
|
|
||||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
|
|
||||||
// 模拟 FindByUsername 找到用户,确认是用户名重复
|
|
||||||
m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeConflict),
|
|
||||||
"message": "用户名已存在",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "创建用户失败_通用数据库错误",
|
|
||||||
requestBody: user.CreateUserRequest{
|
|
||||||
Username: "db_error_user",
|
|
||||||
Password: "password123",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
// 模拟 CreateTx 失败,通用数据库错误
|
|
||||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
|
|
||||||
// 模拟 FindByUsername 找不到用户,确认不是用户名重复
|
|
||||||
m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeInternalError),
|
|
||||||
"message": "创建用户失败",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// 初始化 Gin 上下文和记录器
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
ctx, _ := gin.CreateTestContext(w)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/users", nil) // URL 路径不重要,因为我们不测试路由
|
|
||||||
|
|
||||||
// 设置请求体
|
|
||||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
|
||||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// 创建 Mock UserRepository
|
|
||||||
mockRepo := new(MockUserRepository)
|
|
||||||
|
|
||||||
// 设置 Mock UserRepository 行为
|
|
||||||
tt.mockRepoSetup(mockRepo)
|
|
||||||
|
|
||||||
// 创建控制器实例,使用静默日志器
|
|
||||||
userController := user.NewController(mockRepo, silentLogger, nil) // tokenService 在 CreateUser 中未使用,设为 nil
|
|
||||||
|
|
||||||
// 调用被测试的方法
|
|
||||||
userController.CreateUser(ctx)
|
|
||||||
|
|
||||||
// 解析响应体
|
|
||||||
var responseBody map[string]interface{}
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// 断言响应体中的 code 字段
|
|
||||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
|
||||||
|
|
||||||
// 断言响应内容 (除了 code 字段)
|
|
||||||
if tt.expectedResponse["code"] == float64(controller.CodeCreated) {
|
|
||||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
|
||||||
data, ok := responseBody["data"].(map[string]interface{})
|
|
||||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
|
||||||
// 确保 id 字段存在且不为零
|
|
||||||
id, idOk := data["id"].(float64)
|
|
||||||
assert.True(t, idOk, "响应体中的 data.id 字段应为 float64 类型")
|
|
||||||
assert.NotEqual(t, float64(0), id, "响应体中的 data.id 不应为零")
|
|
||||||
|
|
||||||
// 移除 ID 字段以便进行通用断言
|
|
||||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
|
||||||
// 移除 expectedResponse 中的 id 字段,因为我们已经单独验证了
|
|
||||||
if expectedData, ok := tt.expectedResponse["data"].(map[string]interface{}); ok {
|
|
||||||
delete(expectedData, "id")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 移除 code 字段以便进行通用断言
|
|
||||||
delete(responseBody, "code")
|
|
||||||
delete(tt.expectedResponse, "code")
|
|
||||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
|
||||||
|
|
||||||
// 验证 Mock 期望是否都已满足
|
|
||||||
mockRepo.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogin 测试 Login 方法
|
|
||||||
func TestLogin(t *testing.T) {
|
|
||||||
// 设置release模式阻止废话日志
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
|
|
||||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
|
||||||
silentLogger := logs.NewSilentLogger()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody user.LoginRequest
|
|
||||||
mockRepoSetup func(*MockUserRepository)
|
|
||||||
mockTokenServiceSetup func(*MockTokenService)
|
|
||||||
expectedResponse map[string]interface{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "成功登录",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "correctpassword",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
mockUser := &models.User{
|
|
||||||
Model: gorm.Model{ID: 1},
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
|
||||||
}
|
|
||||||
// 调用 BeforeCreate 钩子来哈希密码
|
|
||||||
_ = mockUser.BeforeCreate(nil)
|
|
||||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
|
||||||
},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
|
||||||
m.On("GenerateToken", uint(1)).Return("mocked_token", nil).Once()
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeSuccess),
|
|
||||||
"message": "登录成功",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"username": "loginuser",
|
|
||||||
"id": float64(1),
|
|
||||||
"token": "mocked_token",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败_缺少用户名",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "", // 缺少用户名
|
|
||||||
Password: "password",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeBadRequest),
|
|
||||||
"message": "Key: 'LoginRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "请求参数绑定失败_缺少密码",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "testuser",
|
|
||||||
Password: "", // 缺少密码
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeBadRequest),
|
|
||||||
"message": "Key: 'LoginRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户不存在",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "nonexistent",
|
|
||||||
Password: "anypassword",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
m.On("FindByUsername", "nonexistent").Return(nil, gorm.ErrRecordNotFound).Once()
|
|
||||||
},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeUnauthorized),
|
|
||||||
"message": "用户名或密码不正确",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询用户失败_通用数据库错误",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "dberroruser",
|
|
||||||
Password: "password",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
m.On("FindByUsername", "dberroruser").Return(nil, errors.New("database connection error")).Once()
|
|
||||||
},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {}, expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeInternalError),
|
|
||||||
"message": "登录失败",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "密码不正确",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "wrongpassword",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
mockUser := &models.User{
|
|
||||||
Model: gorm.Model{ID: 1},
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
|
||||||
}
|
|
||||||
// 调用 BeforeCreate 钩子来哈希密码
|
|
||||||
_ = mockUser.BeforeCreate(nil)
|
|
||||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
|
||||||
},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeUnauthorized),
|
|
||||||
"message": "用户名或密码不正确",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "生成Token失败",
|
|
||||||
requestBody: user.LoginRequest{
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "correctpassword",
|
|
||||||
},
|
|
||||||
mockRepoSetup: func(m *MockUserRepository) {
|
|
||||||
mockUser := &models.User{
|
|
||||||
Model: gorm.Model{ID: 1},
|
|
||||||
Username: "loginuser",
|
|
||||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
|
||||||
}
|
|
||||||
// 调用 BeforeCreate 钩子来哈希密码
|
|
||||||
_ = mockUser.BeforeCreate(nil)
|
|
||||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
|
||||||
},
|
|
||||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
|
||||||
m.On("GenerateToken", uint(1)).Return("", errors.New("jwt error")).Once()
|
|
||||||
},
|
|
||||||
expectedResponse: map[string]interface{}{
|
|
||||||
"code": float64(controller.CodeInternalError),
|
|
||||||
"message": "登录失败,无法生成认证信息",
|
|
||||||
"data": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// 初始化 Gin 上下文和记录器
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
ctx, _ := gin.CreateTestContext(w)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/login", nil) // URL 路径不重要,因为我们不测试路由
|
|
||||||
|
|
||||||
// 设置请求体
|
|
||||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
|
||||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// 创建 Mock
|
|
||||||
mockRepo := new(MockUserRepository)
|
|
||||||
mockTokenService := new(MockTokenService)
|
|
||||||
|
|
||||||
// 设置 Mock 行为
|
|
||||||
tt.mockRepoSetup(mockRepo)
|
|
||||||
tt.mockTokenServiceSetup(mockTokenService)
|
|
||||||
|
|
||||||
// 创建控制器实例
|
|
||||||
userController := user.NewController(mockRepo, silentLogger, mockTokenService)
|
|
||||||
|
|
||||||
// 调用被测试的方法
|
|
||||||
userController.Login(ctx)
|
|
||||||
|
|
||||||
// 解析响应体
|
|
||||||
var responseBody map[string]interface{}
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// 断言响应体中的 code 字段
|
|
||||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
|
||||||
|
|
||||||
// 断言响应内容 (除了 code 字段)
|
|
||||||
if tt.expectedResponse["code"] == float64(controller.CodeSuccess) {
|
|
||||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
|
||||||
data, ok := responseBody["data"].(map[string]interface{})
|
|
||||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
|
||||||
|
|
||||||
// 验证 id 和 token 存在
|
|
||||||
assert.NotNil(t, data["id"])
|
|
||||||
assert.NotNil(t, data["token"])
|
|
||||||
|
|
||||||
// 移除 ID 和 Token 字段以便进行通用断言
|
|
||||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
|
||||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "id")
|
|
||||||
delete(responseBody["data"].(map[string]interface{}), "token")
|
|
||||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "token")
|
|
||||||
}
|
|
||||||
// 移除 code 字段以便进行通用断言
|
|
||||||
delete(responseBody, "code")
|
|
||||||
delete(tt.expectedResponse, "code")
|
|
||||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
|
||||||
|
|
||||||
// 验证 Mock 期望是否都已满足
|
|
||||||
mockRepo.AssertExpectations(t)
|
|
||||||
mockTokenService.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,20 +4,20 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
|||||||
|
|
||||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
|
// CreateDeviceRequest 定义了创建设备时需要传入的参数
|
||||||
type CreateDeviceRequest struct {
|
type CreateDeviceRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
DeviceTemplateID uint `json:"device_template_id" binding:"required"`
|
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||||
AreaControllerID uint `json:"area_controller_id" binding:"required"`
|
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
|
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
|
||||||
type UpdateDeviceRequest struct {
|
type UpdateDeviceRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
DeviceTemplateID uint `json:"device_template_id" binding:"required"`
|
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||||
AreaControllerID uint `json:"area_controller_id" binding:"required"`
|
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManualControlDeviceRequest 定义了手动控制设备时需要传入的参数
|
// ManualControlDeviceRequest 定义了手动控制设备时需要传入的参数
|
||||||
@@ -28,38 +28,38 @@ type ManualControlDeviceRequest struct {
|
|||||||
|
|
||||||
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
|
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
|
||||||
type CreateAreaControllerRequest struct {
|
type CreateAreaControllerRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
NetworkID string `json:"network_id" binding:"required"`
|
NetworkID string `json:"network_id" validate:"required"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
|
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
|
||||||
type UpdateAreaControllerRequest struct {
|
type UpdateAreaControllerRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
NetworkID string `json:"network_id" binding:"required"`
|
NetworkID string `json:"network_id" validate:"required"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
|
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
|
||||||
type CreateDeviceTemplateRequest struct {
|
type CreateDeviceTemplateRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty" validate:"omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Category models.DeviceCategory `json:"category" binding:"required"`
|
Category models.DeviceCategory `json:"category" validate:"required"`
|
||||||
Commands map[string]interface{} `json:"commands" binding:"required"`
|
Commands map[string]interface{} `json:"commands" validate:"required"`
|
||||||
Values []models.ValueDescriptor `json:"values,omitempty"`
|
Values []models.ValueDescriptor `json:"values,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
|
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
|
||||||
type UpdateDeviceTemplateRequest struct {
|
type UpdateDeviceTemplateRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty" validate:"omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Category models.DeviceCategory `json:"category" binding:"required"`
|
Category models.DeviceCategory `json:"category" validate:"required"`
|
||||||
Commands map[string]interface{} `json:"commands" binding:"required"`
|
Commands map[string]interface{} `json:"commands" validate:"required"`
|
||||||
Values []models.ValueDescriptor `json:"values,omitempty"`
|
Values []models.ValueDescriptor `json:"values,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
|
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ type PaginationDTO struct {
|
|||||||
|
|
||||||
// ListSensorDataRequest 定义了获取传感器数据列表的请求参数
|
// ListSensorDataRequest 定义了获取传感器数据列表的请求参数
|
||||||
type ListSensorDataRequest struct {
|
type ListSensorDataRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
DeviceID *uint `form:"device_id"`
|
DeviceID *uint `query:"device_id"`
|
||||||
SensorType *string `form:"sensor_type"`
|
SensorType *string `query:"sensor_type"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SensorDataDTO 是用于API响应的传感器数据结构
|
// SensorDataDTO 是用于API响应的传感器数据结构
|
||||||
@@ -48,13 +48,13 @@ type ListSensorDataResponse struct {
|
|||||||
|
|
||||||
// ListDeviceCommandLogRequest 定义了获取设备命令日志列表的请求参数
|
// ListDeviceCommandLogRequest 定义了获取设备命令日志列表的请求参数
|
||||||
type ListDeviceCommandLogRequest struct {
|
type ListDeviceCommandLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
DeviceID *uint `form:"device_id"`
|
DeviceID *uint `query:"device_id"`
|
||||||
ReceivedSuccess *bool `form:"received_success"`
|
ReceivedSuccess *bool `query:"received_success"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCommandLogDTO 是用于API响应的设备命令日志结构
|
// DeviceCommandLogDTO 是用于API响应的设备命令日志结构
|
||||||
@@ -76,13 +76,13 @@ type ListDeviceCommandLogResponse struct {
|
|||||||
|
|
||||||
// ListPlanExecutionLogRequest 定义了获取计划执行日志列表的请求参数
|
// ListPlanExecutionLogRequest 定义了获取计划执行日志列表的请求参数
|
||||||
type ListPlanExecutionLogRequest struct {
|
type ListPlanExecutionLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PlanID *uint `form:"plan_id"`
|
PlanID *uint `query:"plan_id"`
|
||||||
Status *string `form:"status"`
|
Status *string `query:"status"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanExecutionLogDTO 是用于API响应的计划执行日志结构
|
// PlanExecutionLogDTO 是用于API响应的计划执行日志结构
|
||||||
@@ -108,14 +108,14 @@ type ListPlanExecutionLogResponse struct {
|
|||||||
|
|
||||||
// ListTaskExecutionLogRequest 定义了获取任务执行日志列表的请求参数
|
// ListTaskExecutionLogRequest 定义了获取任务执行日志列表的请求参数
|
||||||
type ListTaskExecutionLogRequest struct {
|
type ListTaskExecutionLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PlanExecutionLogID *uint `form:"plan_execution_log_id"`
|
PlanExecutionLogID *uint `query:"plan_execution_log_id"`
|
||||||
TaskID *int `form:"task_id"`
|
TaskID *int `query:"task_id"`
|
||||||
Status *string `form:"status"`
|
Status *string `query:"status"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskDTO 是用于API响应的简化版任务结构
|
// TaskDTO 是用于API响应的简化版任务结构
|
||||||
@@ -149,13 +149,13 @@ type ListTaskExecutionLogResponse struct {
|
|||||||
|
|
||||||
// ListPendingCollectionRequest 定义了获取待采集请求列表的请求参数
|
// ListPendingCollectionRequest 定义了获取待采集请求列表的请求参数
|
||||||
type ListPendingCollectionRequest struct {
|
type ListPendingCollectionRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
DeviceID *uint `form:"device_id"`
|
DeviceID *uint `query:"device_id"`
|
||||||
Status *string `form:"status"`
|
Status *string `query:"status"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PendingCollectionDTO 是用于API响应的待采集请求结构
|
// PendingCollectionDTO 是用于API响应的待采集请求结构
|
||||||
@@ -178,15 +178,15 @@ type ListPendingCollectionResponse struct {
|
|||||||
|
|
||||||
// ListUserActionLogRequest 定义了获取用户操作日志列表的请求参数
|
// ListUserActionLogRequest 定义了获取用户操作日志列表的请求参数
|
||||||
type ListUserActionLogRequest struct {
|
type ListUserActionLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
UserID *uint `form:"user_id"`
|
UserID *uint `query:"user_id"`
|
||||||
Username *string `form:"username"`
|
Username *string `query:"username"`
|
||||||
ActionType *string `form:"action_type"`
|
ActionType *string `query:"action_type"`
|
||||||
Status *string `form:"status"`
|
Status *string `query:"status"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserActionLogDTO 是用于API响应的用户操作日志结构
|
// UserActionLogDTO 是用于API响应的用户操作日志结构
|
||||||
@@ -215,13 +215,13 @@ type ListUserActionLogResponse struct {
|
|||||||
|
|
||||||
// ListRawMaterialPurchaseRequest 定义了获取原料采购列表的请求参数
|
// ListRawMaterialPurchaseRequest 定义了获取原料采购列表的请求参数
|
||||||
type ListRawMaterialPurchaseRequest struct {
|
type ListRawMaterialPurchaseRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
RawMaterialID *uint `form:"raw_material_id"`
|
RawMaterialID *uint `query:"raw_material_id"`
|
||||||
Supplier *string `form:"supplier"`
|
Supplier *string `query:"supplier"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMaterialDTO 是用于API响应的简化版原料结构
|
// RawMaterialDTO 是用于API响应的简化版原料结构
|
||||||
@@ -253,14 +253,14 @@ type ListRawMaterialPurchaseResponse struct {
|
|||||||
|
|
||||||
// ListRawMaterialStockLogRequest 定义了获取原料库存日志列表的请求参数
|
// ListRawMaterialStockLogRequest 定义了获取原料库存日志列表的请求参数
|
||||||
type ListRawMaterialStockLogRequest struct {
|
type ListRawMaterialStockLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
RawMaterialID *uint `form:"raw_material_id"`
|
RawMaterialID *uint `query:"raw_material_id"`
|
||||||
SourceType *string `form:"source_type"`
|
SourceType *string `query:"source_type"`
|
||||||
SourceID *uint `form:"source_id"`
|
SourceID *uint `query:"source_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
|
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
|
||||||
@@ -284,14 +284,14 @@ type ListRawMaterialStockLogResponse struct {
|
|||||||
|
|
||||||
// ListFeedUsageRecordRequest 定义了获取饲料使用记录列表的请求参数
|
// ListFeedUsageRecordRequest 定义了获取饲料使用记录列表的请求参数
|
||||||
type ListFeedUsageRecordRequest struct {
|
type ListFeedUsageRecordRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PenID *uint `form:"pen_id"`
|
PenID *uint `query:"pen_id"`
|
||||||
FeedFormulaID *uint `form:"feed_formula_id"`
|
FeedFormulaID *uint `query:"feed_formula_id"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PenDTO 是用于API响应的简化版猪栏结构
|
// PenDTO 是用于API响应的简化版猪栏结构
|
||||||
@@ -329,15 +329,15 @@ type ListFeedUsageRecordResponse struct {
|
|||||||
|
|
||||||
// ListMedicationLogRequest 定义了获取用药记录列表的请求参数
|
// ListMedicationLogRequest 定义了获取用药记录列表的请求参数
|
||||||
type ListMedicationLogRequest struct {
|
type ListMedicationLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
MedicationID *uint `form:"medication_id"`
|
MedicationID *uint `query:"medication_id"`
|
||||||
Reason *string `form:"reason"`
|
Reason *string `query:"reason"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MedicationDTO 是用于API响应的简化版药品结构
|
// MedicationDTO 是用于API响应的简化版药品结构
|
||||||
@@ -370,14 +370,14 @@ type ListMedicationLogResponse struct {
|
|||||||
|
|
||||||
// ListPigBatchLogRequest 定义了获取猪批次日志列表的请求参数
|
// ListPigBatchLogRequest 定义了获取猪批次日志列表的请求参数
|
||||||
type ListPigBatchLogRequest struct {
|
type ListPigBatchLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
ChangeType *string `form:"change_type"`
|
ChangeType *string `query:"change_type"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigBatchLogDTO 是用于API响应的猪批次日志结构
|
// PigBatchLogDTO 是用于API响应的猪批次日志结构
|
||||||
@@ -405,12 +405,12 @@ type ListPigBatchLogResponse struct {
|
|||||||
|
|
||||||
// ListWeighingBatchRequest 定义了获取批次称重记录列表的请求参数
|
// ListWeighingBatchRequest 定义了获取批次称重记录列表的请求参数
|
||||||
type ListWeighingBatchRequest struct {
|
type ListWeighingBatchRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WeighingBatchDTO 是用于API响应的批次称重记录结构
|
// WeighingBatchDTO 是用于API响应的批次称重记录结构
|
||||||
@@ -433,14 +433,14 @@ type ListWeighingBatchResponse struct {
|
|||||||
|
|
||||||
// ListWeighingRecordRequest 定义了获取单次称重记录列表的请求参数
|
// ListWeighingRecordRequest 定义了获取单次称重记录列表的请求参数
|
||||||
type ListWeighingRecordRequest struct {
|
type ListWeighingRecordRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
WeighingBatchID *uint `form:"weighing_batch_id"`
|
WeighingBatchID *uint `query:"weighing_batch_id"`
|
||||||
PenID *uint `form:"pen_id"`
|
PenID *uint `query:"pen_id"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WeighingRecordDTO 是用于API响应的单次称重记录结构
|
// WeighingRecordDTO 是用于API响应的单次称重记录结构
|
||||||
@@ -466,16 +466,16 @@ type ListWeighingRecordResponse struct {
|
|||||||
|
|
||||||
// ListPigTransferLogRequest 定义了获取猪只迁移日志列表的请求参数
|
// ListPigTransferLogRequest 定义了获取猪只迁移日志列表的请求参数
|
||||||
type ListPigTransferLogRequest struct {
|
type ListPigTransferLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
PenID *uint `form:"pen_id"`
|
PenID *uint `query:"pen_id"`
|
||||||
TransferType *string `form:"transfer_type"`
|
TransferType *string `query:"transfer_type"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
CorrelationID *string `form:"correlation_id"`
|
CorrelationID *string `query:"correlation_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigTransferLogDTO 是用于API响应的猪只迁移日志结构
|
// PigTransferLogDTO 是用于API响应的猪只迁移日志结构
|
||||||
@@ -503,16 +503,16 @@ type ListPigTransferLogResponse struct {
|
|||||||
|
|
||||||
// ListPigSickLogRequest 定义了获取病猪日志列表的请求参数
|
// ListPigSickLogRequest 定义了获取病猪日志列表的请求参数
|
||||||
type ListPigSickLogRequest struct {
|
type ListPigSickLogRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
PenID *uint `form:"pen_id"`
|
PenID *uint `query:"pen_id"`
|
||||||
Reason *string `form:"reason"`
|
Reason *string `query:"reason"`
|
||||||
TreatmentLocation *string `form:"treatment_location"`
|
TreatmentLocation *string `query:"treatment_location"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigSickLogDTO 是用于API响应的病猪日志结构
|
// PigSickLogDTO 是用于API响应的病猪日志结构
|
||||||
@@ -542,14 +542,14 @@ type ListPigSickLogResponse struct {
|
|||||||
|
|
||||||
// ListPigPurchaseRequest 定义了获取猪只采购记录列表的请求参数
|
// ListPigPurchaseRequest 定义了获取猪只采购记录列表的请求参数
|
||||||
type ListPigPurchaseRequest struct {
|
type ListPigPurchaseRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
Supplier *string `form:"supplier"`
|
Supplier *string `query:"supplier"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigPurchaseDTO 是用于API响应的猪只采购记录结构
|
// PigPurchaseDTO 是用于API响应的猪只采购记录结构
|
||||||
@@ -577,14 +577,14 @@ type ListPigPurchaseResponse struct {
|
|||||||
|
|
||||||
// ListPigSaleRequest 定义了获取猪只销售记录列表的请求参数
|
// ListPigSaleRequest 定义了获取猪只销售记录列表的请求参数
|
||||||
type ListPigSaleRequest struct {
|
type ListPigSaleRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
PigBatchID *uint `form:"pig_batch_id"`
|
PigBatchID *uint `query:"pig_batch_id"`
|
||||||
Buyer *string `form:"buyer"`
|
Buyer *string `query:"buyer"`
|
||||||
OperatorID *uint `form:"operator_id"`
|
OperatorID *uint `query:"operator_id"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigSaleDTO 是用于API响应的猪只销售记录结构
|
// PigSaleDTO 是用于API响应的猪只销售记录结构
|
||||||
|
|||||||
@@ -11,20 +11,20 @@ import (
|
|||||||
// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构
|
// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构
|
||||||
type SendTestNotificationRequest struct {
|
type SendTestNotificationRequest struct {
|
||||||
// Type 指定要测试的通知渠道
|
// Type 指定要测试的通知渠道
|
||||||
Type notify.NotifierType `json:"type" binding:"required"`
|
Type notify.NotifierType `json:"type" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListNotificationRequest 定义了获取通知列表的请求参数
|
// ListNotificationRequest 定义了获取通知列表的请求参数
|
||||||
type ListNotificationRequest struct {
|
type ListNotificationRequest struct {
|
||||||
Page int `form:"page,default=1"`
|
Page int `query:"page"`
|
||||||
PageSize int `form:"pageSize,default=10"`
|
PageSize int `query:"pageSize"`
|
||||||
UserID *uint `form:"user_id"`
|
UserID *uint `query:"user_id"`
|
||||||
NotifierType *notify.NotifierType `form:"notifier_type"`
|
NotifierType *notify.NotifierType `query:"notifier_type"`
|
||||||
Status *models.NotificationStatus `form:"status"`
|
Status *models.NotificationStatus `query:"status"`
|
||||||
Level *zapcore.Level `form:"level"`
|
Level *zapcore.Level `query:"level"`
|
||||||
StartTime *time.Time `form:"start_time"`
|
StartTime *time.Time `query:"start_time"`
|
||||||
EndTime *time.Time `form:"end_time"`
|
EndTime *time.Time `query:"end_time"`
|
||||||
OrderBy string `form:"order_by"`
|
OrderBy string `query:"order_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotificationDTO 是用于API响应的通知结构
|
// NotificationDTO 是用于API响应的通知结构
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
|
|
||||||
// PigBatchCreateDTO 定义了创建猪批次的请求结构
|
// PigBatchCreateDTO 定义了创建猪批次的请求结构
|
||||||
type PigBatchCreateDTO struct {
|
type PigBatchCreateDTO struct {
|
||||||
BatchNumber string `json:"batch_number" binding:"required"` // 批次编号,必填
|
BatchNumber string `json:"batch_number" validate:"required"` // 批次编号,必填
|
||||||
OriginType models.PigBatchOriginType `json:"origin_type" binding:"required"` // 批次来源,必填
|
OriginType models.PigBatchOriginType `json:"origin_type" validate:"required"` // 批次来源,必填
|
||||||
StartDate time.Time `json:"start_date" binding:"required"` // 批次开始日期,必填
|
StartDate time.Time `json:"start_date" validate:"required"` // 批次开始日期,必填
|
||||||
InitialCount int `json:"initial_count" binding:"required,min=1"` // 初始数量,必填,最小为1
|
InitialCount int `json:"initial_count" validate:"required,min=1"` // 初始数量,必填,最小为1
|
||||||
Status models.PigBatchStatus `json:"status" binding:"required"` // 批次状态,必填
|
Status models.PigBatchStatus `json:"status" validate:"required"` // 批次状态,必填
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigBatchUpdateDTO 定义了更新猪批次的请求结构
|
// PigBatchUpdateDTO 定义了更新猪批次的请求结构
|
||||||
@@ -27,7 +27,7 @@ type PigBatchUpdateDTO struct {
|
|||||||
|
|
||||||
// PigBatchQueryDTO 定义了查询猪批次的请求结构
|
// PigBatchQueryDTO 定义了查询猪批次的请求结构
|
||||||
type PigBatchQueryDTO struct {
|
type PigBatchQueryDTO struct {
|
||||||
IsActive *bool `json:"is_active" form:"is_active"` // 是否活跃,可选,用于URL查询参数
|
IsActive *bool `json:"is_active" query:"is_active"` // 是否活跃,可选,用于URL查询参数
|
||||||
}
|
}
|
||||||
|
|
||||||
// PigBatchResponseDTO 定义了猪批次信息的响应结构
|
// PigBatchResponseDTO 定义了猪批次信息的响应结构
|
||||||
@@ -48,115 +48,115 @@ type PigBatchResponseDTO struct {
|
|||||||
|
|
||||||
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
|
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
|
||||||
type AssignEmptyPensToBatchRequest struct {
|
type AssignEmptyPensToBatchRequest struct {
|
||||||
PenIDs []uint `json:"penIDs" binding:"required,min=1" example:"[1,2,3]"` // 待分配的猪栏ID列表
|
PenIDs []uint `json:"penIDs" validate:"required,min=1,dive" example:"[1,2,3]"` // 待分配的猪栏ID列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
|
// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
|
||||||
type ReclassifyPenToNewBatchRequest struct {
|
type ReclassifyPenToNewBatchRequest struct {
|
||||||
ToBatchID uint `json:"toBatchID" binding:"required"` // 目标猪批次ID
|
ToBatchID uint `json:"toBatchID" validate:"required"` // 目标猪批次ID
|
||||||
PenID uint `json:"penID" binding:"required"` // 待划拨的猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 待划拨的猪栏ID
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
|
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
|
||||||
type RemoveEmptyPenFromBatchRequest struct {
|
type RemoveEmptyPenFromBatchRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 待移除的猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 待移除的猪栏ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
|
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
|
||||||
type MovePigsIntoPenRequest struct {
|
type MovePigsIntoPenRequest struct {
|
||||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
ToPenID uint `json:"toPenID" validate:"required"` // 目标猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 移入猪只数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 移入猪只数量
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// SellPigsRequest 用于处理卖猪的请求体
|
// SellPigsRequest 用于处理卖猪的请求体
|
||||||
type SellPigsRequest struct {
|
type SellPigsRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 卖出猪只数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 卖出猪只数量
|
||||||
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
|
UnitPrice float64 `json:"unitPrice" validate:"required,min=0"` // 单价
|
||||||
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
|
TotalPrice float64 `json:"totalPrice" validate:"required,min=0"` // 总价
|
||||||
TraderName string `json:"traderName" binding:"required"` // 交易方名称
|
TraderName string `json:"traderName" validate:"required"` // 交易方名称
|
||||||
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
|
TradeDate time.Time `json:"tradeDate" validate:"required"` // 交易日期
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuyPigsRequest 用于处理买猪的请求体
|
// BuyPigsRequest 用于处理买猪的请求体
|
||||||
type BuyPigsRequest struct {
|
type BuyPigsRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 买入猪只数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 买入猪只数量
|
||||||
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
|
UnitPrice float64 `json:"unitPrice" validate:"required,min=0"` // 单价
|
||||||
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
|
TotalPrice float64 `json:"totalPrice" validate:"required,min=0"` // 总价
|
||||||
TraderName string `json:"traderName" binding:"required"` // 交易方名称
|
TraderName string `json:"traderName" validate:"required"` // 交易方名称
|
||||||
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
|
TradeDate time.Time `json:"tradeDate" validate:"required"` // 交易日期
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
|
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
|
||||||
type TransferPigsAcrossBatchesRequest struct {
|
type TransferPigsAcrossBatchesRequest struct {
|
||||||
DestBatchID uint `json:"destBatchID" binding:"required"` // 目标猪批次ID
|
DestBatchID uint `json:"destBatchID" validate:"required"` // 目标猪批次ID
|
||||||
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
|
FromPenID uint `json:"fromPenID" validate:"required"` // 源猪栏ID
|
||||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
ToPenID uint `json:"toPenID" validate:"required"` // 目标猪栏ID
|
||||||
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
|
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransferPigsWithinBatchRequest 用于群内调栏的请求体
|
// TransferPigsWithinBatchRequest 用于群内调栏的请求体
|
||||||
type TransferPigsWithinBatchRequest struct {
|
type TransferPigsWithinBatchRequest struct {
|
||||||
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
|
FromPenID uint `json:"fromPenID" validate:"required"` // 源猪栏ID
|
||||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
ToPenID uint `json:"toPenID" validate:"required"` // 目标猪栏ID
|
||||||
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
|
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordSickPigsRequest 用于记录新增病猪事件的请求体
|
// RecordSickPigsRequest 用于记录新增病猪事件的请求体
|
||||||
type RecordSickPigsRequest struct {
|
type RecordSickPigsRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 病猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 病猪数量
|
||||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" validate:"required"` // 治疗地点
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
|
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
|
||||||
type RecordSickPigRecoveryRequest struct {
|
type RecordSickPigRecoveryRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 康复猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 康复猪数量
|
||||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" validate:"required"` // 治疗地点
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
|
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
|
||||||
type RecordSickPigDeathRequest struct {
|
type RecordSickPigDeathRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
|
||||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" validate:"required"` // 治疗地点
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
|
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
|
||||||
type RecordSickPigCullRequest struct {
|
type RecordSickPigCullRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
|
||||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" validate:"required"` // 治疗地点
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
|
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
|
||||||
type RecordDeathRequest struct {
|
type RecordDeathRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
|
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
|
||||||
type RecordCullRequest struct {
|
type RecordCullRequest struct {
|
||||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
PenID uint `json:"penID" validate:"required"` // 猪栏ID
|
||||||
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
|
Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
|
||||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
HappenedAt time.Time `json:"happenedAt" validate:"required"` // 发生时间
|
||||||
Remarks string `json:"remarks"` // 备注
|
Remarks string `json:"remarks"` // 备注
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,32 +22,32 @@ type PenResponse struct {
|
|||||||
|
|
||||||
// CreatePigHouseRequest 定义了创建猪舍的请求结构
|
// CreatePigHouseRequest 定义了创建猪舍的请求结构
|
||||||
type CreatePigHouseRequest struct {
|
type CreatePigHouseRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePigHouseRequest 定义了更新猪舍的请求结构
|
// UpdatePigHouseRequest 定义了更新猪舍的请求结构
|
||||||
type UpdatePigHouseRequest struct {
|
type UpdatePigHouseRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePenRequest 定义了创建猪栏的请求结构
|
// CreatePenRequest 定义了创建猪栏的请求结构
|
||||||
type CreatePenRequest struct {
|
type CreatePenRequest struct {
|
||||||
PenNumber string `json:"pen_number" binding:"required"`
|
PenNumber string `json:"pen_number" validate:"required"`
|
||||||
HouseID uint `json:"house_id" binding:"required"`
|
HouseID uint `json:"house_id" validate:"required"`
|
||||||
Capacity int `json:"capacity" binding:"required"`
|
Capacity int `json:"capacity" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePenRequest 定义了更新猪栏的请求结构
|
// UpdatePenRequest 定义了更新猪栏的请求结构
|
||||||
type UpdatePenRequest struct {
|
type UpdatePenRequest struct {
|
||||||
PenNumber string `json:"pen_number" binding:"required"`
|
PenNumber string `json:"pen_number" validate:"required"`
|
||||||
HouseID uint `json:"house_id" binding:"required"`
|
HouseID uint `json:"house_id" validate:"required"`
|
||||||
Capacity int `json:"capacity" binding:"required"`
|
Capacity int `json:"capacity" validate:"required"`
|
||||||
Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
|
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构
|
// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构
|
||||||
type UpdatePenStatusRequest struct {
|
type UpdatePenStatusRequest struct {
|
||||||
Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
|
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ import (
|
|||||||
|
|
||||||
// ListPlansQuery 定义了获取计划列表时的查询参数
|
// ListPlansQuery 定义了获取计划列表时的查询参数
|
||||||
type ListPlansQuery struct {
|
type ListPlansQuery struct {
|
||||||
PlanType repository.PlanTypeFilter `form:"planType,default=自定义任务"` // 计划类型
|
PlanType repository.PlanTypeFilter `query:"planType"` // 计划类型
|
||||||
Page int `form:"page,default=1"` // 页码
|
Page int `query:"page"` // 页码
|
||||||
PageSize int `form:"pageSize,default=10"` // 每页大小
|
PageSize int `query:"pageSize"` // 每页大小
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePlanRequest 定义创建计划请求的结构体
|
// CreatePlanRequest 定义创建计划请求的结构体
|
||||||
type CreatePlanRequest struct {
|
type CreatePlanRequest struct {
|
||||||
Name string `json:"name" binding:"required" example:"猪舍温度控制计划"`
|
Name string `json:"name" validate:"required" example:"猪舍温度控制计划"`
|
||||||
Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
|
Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
|
||||||
ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
|
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||||
ExecuteNum uint `json:"execute_num,omitempty" example:"10"`
|
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||||
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
|
CronExpression string `json:"cron_expression" validate:"omitempty,cron" example:"0 0 6 * * *"`
|
||||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty"`
|
SubPlanIDs []uint `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||||
Tasks []TaskRequest `json:"tasks,omitempty"`
|
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanResponse 定义计划详情响应的结构体
|
// PlanResponse 定义计划详情响应的结构体
|
||||||
@@ -49,11 +49,11 @@ type ListPlansResponse struct {
|
|||||||
type UpdatePlanRequest struct {
|
type UpdatePlanRequest struct {
|
||||||
Name string `json:"name" example:"猪舍温度控制计划V2"`
|
Name string `json:"name" example:"猪舍温度控制计划V2"`
|
||||||
Description string `json:"description" example:"更新后的描述"`
|
Description string `json:"description" example:"更新后的描述"`
|
||||||
ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
|
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||||
ExecuteNum uint `json:"execute_num,omitempty" example:"10"`
|
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||||
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
|
CronExpression string `json:"cron_expression" validate:"omitempty,cron" example:"0 0 6 * * *"`
|
||||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty"`
|
SubPlanIDs []uint `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||||
Tasks []TaskRequest `json:"tasks,omitempty"`
|
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubPlanResponse 定义子计划响应结构体
|
// SubPlanResponse 定义子计划响应结构体
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package dto
|
|||||||
|
|
||||||
// CreateUserRequest 定义创建用户请求的结构体
|
// CreateUserRequest 定义创建用户请求的结构体
|
||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
Username string `json:"username" binding:"required" example:"newuser"`
|
Username string `json:"username" validate:"required" example:"newuser"`
|
||||||
Password string `json:"password" binding:"required" example:"password123"`
|
Password string `json:"password" validate:"required" example:"password123"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginRequest 定义登录请求的结构体
|
// LoginRequest 定义登录请求的结构体
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
|
// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
|
||||||
Identifier string `json:"identifier" binding:"required" example:"testuser"`
|
Identifier string `json:"identifier" validate:"required" example:"testuser"`
|
||||||
Password string `json:"password" binding:"required" example:"password123"`
|
Password string `json:"password" validate:"required" example:"password123"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUserResponse 定义创建用户成功响应的结构体
|
// CreateUserResponse 定义创建用户成功响应的结构体
|
||||||
|
|||||||
@@ -1,117 +1,59 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
|
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type auditResponse struct {
|
// AuditLogMiddleware 创建一个Echo中间件,用于在请求结束后记录用户操作审计日志。
|
||||||
Code int `json:"code"`
|
// 它依赖于控制器通过调用 SendSuccessWithAudit 或 SendErrorWithAudit 在上下文中设置的审计信息。
|
||||||
Message string `json:"message"`
|
func AuditLogMiddleware(auditService audit.Service) echo.MiddlewareFunc {
|
||||||
}
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
// AuditLogMiddleware 创建一个Gin中间件,用于在请求结束后记录用户操作审计日志
|
|
||||||
func AuditLogMiddleware(auditService audit.Service) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// 使用自定义的 response body writer 来捕获响应体
|
|
||||||
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
|
||||||
c.Writer = blw
|
|
||||||
|
|
||||||
// 首先执行请求链中的后续处理程序(即业务控制器)
|
// 首先执行请求链中的后续处理程序(即业务控制器)
|
||||||
c.Next()
|
err := next(c)
|
||||||
|
|
||||||
// --- 在这里,请求已经处理完毕 ---
|
// --- 在这里,请求已经处理完毕 ---
|
||||||
|
|
||||||
// 从上下文中尝试获取由控制器设置的业务审计信息
|
// 从上下文中尝试获取由控制器设置的业务审计信息
|
||||||
actionType, exists := c.Get(models.ContextAuditActionType.String())
|
actionType, exists := c.Get(models.ContextAuditActionType.String()).(string)
|
||||||
if !exists {
|
if !exists || actionType == "" {
|
||||||
// 如果上下文中没有 actionType,说明此接口无需记录审计日志,直接返回
|
// 如果上下文中没有 actionType,说明此接口无需记录审计日志,直接返回
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Gin Context 中获取用户对象
|
// 从 Context 中获取用户对象
|
||||||
userCtx, userExists := c.Get(models.ContextUserKey.String())
|
|
||||||
var user *models.User
|
var user *models.User
|
||||||
if userExists {
|
if userCtx := c.Get(models.ContextUserKey.String()); userCtx != nil {
|
||||||
user, _ = userCtx.(*models.User)
|
user, _ = userCtx.(*models.User)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 RequestContext
|
// 构建 RequestContext
|
||||||
reqCtx := audit.RequestContext{
|
reqCtx := audit.RequestContext{
|
||||||
ClientIP: c.ClientIP(),
|
ClientIP: c.RealIP(),
|
||||||
HTTPPath: c.Request.URL.Path,
|
HTTPPath: c.Request().URL.Path,
|
||||||
HTTPMethod: c.Request.Method,
|
HTTPMethod: c.Request().Method,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取其他审计信息
|
// 直接从上下文中获取所有其他审计信息
|
||||||
description, _ := c.Get(models.ContextAuditDescription.String())
|
description, _ := c.Get(models.ContextAuditDescription.String()).(string)
|
||||||
targetResource, _ := c.Get(models.ContextAuditTargetResource.String())
|
targetResource := c.Get(models.ContextAuditTargetResource.String())
|
||||||
|
status, _ := c.Get(models.ContextAuditStatus.String()).(models.AuditStatus)
|
||||||
// 默认操作状态为成功
|
resultDetails, _ := c.Get(models.ContextAuditResultDetails.String()).(string)
|
||||||
status := models.AuditStatusSuccess
|
|
||||||
resultDetails := ""
|
|
||||||
|
|
||||||
// 尝试从捕获的响应体中解析平台响应
|
|
||||||
var platformResponse auditResponse
|
|
||||||
if err := json.Unmarshal(blw.body.Bytes(), &platformResponse); err == nil {
|
|
||||||
// 如果解析成功,根据平台状态码判断操作是否失败
|
|
||||||
// 成功状态码范围是 2000-2999
|
|
||||||
if platformResponse.Code < 2000 || platformResponse.Code >= 3000 {
|
|
||||||
status = models.AuditStatusFailed
|
|
||||||
resultDetails = platformResponse.Message
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果响应体不是预期的平台响应格式,或者解析失败,则记录原始HTTP状态码作为详情
|
|
||||||
// 并且如果HTTP状态码不是2xx,则标记为失败
|
|
||||||
if c.Writer.Status() < 200 || c.Writer.Status() >= 300 {
|
|
||||||
status = models.AuditStatusFailed
|
|
||||||
}
|
|
||||||
resultDetails = "HTTP Status: " + strconv.Itoa(c.Writer.Status()) + ", Body Parse Error: " + err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用审计服务记录日志(异步)
|
// 调用审计服务记录日志(异步)
|
||||||
auditService.LogAction(
|
auditService.LogAction(
|
||||||
user,
|
user,
|
||||||
reqCtx,
|
reqCtx,
|
||||||
actionType.(string),
|
actionType,
|
||||||
description.(string),
|
description,
|
||||||
targetResource,
|
targetResource,
|
||||||
status,
|
status,
|
||||||
resultDetails,
|
resultDetails,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bodyLogWriter 是一个自定义的 gin.ResponseWriter,用于捕获响应体
|
|
||||||
// 这对于在操作失败时记录详细的错误信息非常有用
|
|
||||||
type bodyLogWriter struct {
|
|
||||||
gin.ResponseWriter
|
|
||||||
body *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w bodyLogWriter) Write(b []byte) (int, error) {
|
|
||||||
w.body.Write(b)
|
|
||||||
return w.ResponseWriter.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w bodyLogWriter) WriteString(s string) (int, error) {
|
|
||||||
w.body.WriteString(s)
|
|
||||||
return w.ResponseWriter.WriteString(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBody 用于安全地读取请求体,并防止其被重复读取
|
|
||||||
func ReadBody(c *gin.Context) ([]byte, error) {
|
|
||||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 将读取的内容放回 Body 中,以便后续的处理函数可以再次读取
|
|
||||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
||||||
return bodyBytes, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
// Package middleware 存放 gin 中间件
|
// Package middleware 存放中间件
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/labstack/echo/v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthMiddleware 创建一个Gin中间件,用于JWT身份验证
|
// AuthMiddleware 创建一个Echo中间件,用于JWT身份验证
|
||||||
// 它依赖于 TokenService 来解析和验证 token,并使用 UserRepository 来获取完整的用户信息
|
// 它依赖于 TokenService 来解析和验证 token,并使用 UserRepository 来获取完整的用户信息
|
||||||
func AuthMiddleware(tokenService token.Service, userRepo repository.UserRepository) gin.HandlerFunc {
|
func AuthMiddleware(tokenService token.Service, userRepo repository.UserRepository) echo.MiddlewareFunc {
|
||||||
return func(c *gin.Context) {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
// 从 Authorization header 获取 token
|
// 从 Authorization header 获取 token
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.Request().Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "请求未包含授权标头"})
|
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "请求未包含授权标头")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 授权标头的格式应为 "Bearer <token>"
|
// 授权标头的格式应为 "Bearer <token>"
|
||||||
parts := strings.Split(authHeader, " ")
|
parts := strings.Split(authHeader, " ")
|
||||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "授权标头格式不正确"})
|
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "授权标头格式不正确")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenString := parts[1]
|
tokenString := parts[1]
|
||||||
@@ -35,27 +36,25 @@ func AuthMiddleware(tokenService token.Service, userRepo repository.UserReposito
|
|||||||
// 解析和验证 token
|
// 解析和验证 token
|
||||||
claims, err := tokenService.ParseToken(tokenString)
|
claims, err := tokenService.ParseToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的Token"})
|
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "无效的Token")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 token 中的用户ID,从数据库中获取完整的用户信息
|
// 根据 token 中的用户ID,从数据库中获取完整的用户信息
|
||||||
user, err := userRepo.FindByID(claims.UserID)
|
user, err := userRepo.FindByID(claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// Token有效,但对应的用户已不存在
|
// Token有效,但对应的用户已不存在
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "授权用户不存在"})
|
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "授权用户不存在")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 其他数据库查询错误
|
// 其他数据库查询错误
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"})
|
return controller.SendErrorWithStatus(c, http.StatusInternalServerError, controller.CodeInternalError, "获取用户信息失败")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将完整的用户对象存储在 context 中,以便后续的处理函数使用
|
// 将完整的用户对象存储在 context 中,以便后续的处理函数使用
|
||||||
c.Set(models.ContextUserKey.String(), user)
|
c.Set(models.ContextUserKey.String(), user)
|
||||||
|
|
||||||
// 继续处理请求链中的下一个处理程序
|
// 继续处理请求链中的下一个处理程序
|
||||||
c.Next()
|
return next(c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ const (
|
|||||||
ContextAuditActionType AuditContextKey = "auditActionType"
|
ContextAuditActionType AuditContextKey = "auditActionType"
|
||||||
ContextAuditTargetResource AuditContextKey = "auditTargetResource"
|
ContextAuditTargetResource AuditContextKey = "auditTargetResource"
|
||||||
ContextAuditDescription AuditContextKey = "auditDescription"
|
ContextAuditDescription AuditContextKey = "auditDescription"
|
||||||
|
ContextAuditStatus AuditContextKey = "auditStatus"
|
||||||
|
ContextAuditResultDetails AuditContextKey = "auditResultDetails"
|
||||||
|
|
||||||
ContextUserKey AuditContextKey = "user"
|
ContextUserKey AuditContextKey = "user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
454
openspec/AGENTS.md
Normal file
454
openspec/AGENTS.md
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||||
|
|
||||||
|
## TL;DR Quick Checklist
|
||||||
|
|
||||||
|
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||||
|
- Decide scope: new capability vs modify existing capability
|
||||||
|
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||||
|
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||||
|
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||||
|
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||||
|
- Request approval: Do not start implementation until proposal is approved
|
||||||
|
|
||||||
|
## Three-Stage Workflow
|
||||||
|
|
||||||
|
### Stage 1: Creating Changes
|
||||||
|
Create proposal when you need to:
|
||||||
|
- Add features or functionality
|
||||||
|
- Make breaking changes (API, schema)
|
||||||
|
- Change architecture or patterns
|
||||||
|
- Optimize performance (changes behavior)
|
||||||
|
- Update security patterns
|
||||||
|
|
||||||
|
Triggers (examples):
|
||||||
|
- "Help me create a change proposal"
|
||||||
|
- "Help me plan a change"
|
||||||
|
- "Help me create a proposal"
|
||||||
|
- "I want to create a spec proposal"
|
||||||
|
- "I want to create a spec"
|
||||||
|
|
||||||
|
Loose matching guidance:
|
||||||
|
- Contains one of: `proposal`, `change`, `spec`
|
||||||
|
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||||
|
|
||||||
|
Skip proposal for:
|
||||||
|
- Bug fixes (restore intended behavior)
|
||||||
|
- Typos, formatting, comments
|
||||||
|
- Dependency updates (non-breaking)
|
||||||
|
- Configuration changes
|
||||||
|
- Tests for existing behavior
|
||||||
|
|
||||||
|
**Workflow**
|
||||||
|
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||||
|
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||||
|
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||||
|
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||||
|
|
||||||
|
### Stage 2: Implementing Changes
|
||||||
|
Track these steps as TODOs and complete them one by one.
|
||||||
|
1. **Read proposal.md** - Understand what's being built
|
||||||
|
2. **Read design.md** (if exists) - Review technical decisions
|
||||||
|
3. **Read tasks.md** - Get implementation checklist
|
||||||
|
4. **Implement tasks sequentially** - Complete in order
|
||||||
|
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||||
|
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||||
|
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||||
|
|
||||||
|
### Stage 3: Archiving Changes
|
||||||
|
After deployment, create separate PR to:
|
||||||
|
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||||
|
- Update `specs/` if capabilities changed
|
||||||
|
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||||
|
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||||
|
|
||||||
|
## Before Any Task
|
||||||
|
|
||||||
|
**Context Checklist:**
|
||||||
|
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||||
|
- [ ] Check pending changes in `changes/` for conflicts
|
||||||
|
- [ ] Read `openspec/project.md` for conventions
|
||||||
|
- [ ] Run `openspec list` to see active changes
|
||||||
|
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||||
|
|
||||||
|
**Before Creating Specs:**
|
||||||
|
- Always check if capability already exists
|
||||||
|
- Prefer modifying existing specs over creating duplicates
|
||||||
|
- Use `openspec show [spec]` to review current state
|
||||||
|
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||||
|
|
||||||
|
### Search Guidance
|
||||||
|
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||||
|
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||||
|
- Show details:
|
||||||
|
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||||
|
- Change: `openspec show <change-id> --json --deltas-only`
|
||||||
|
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Essential commands
|
||||||
|
openspec list # List active changes
|
||||||
|
openspec list --specs # List specifications
|
||||||
|
openspec show [item] # Display change or spec
|
||||||
|
openspec validate [item] # Validate changes or specs
|
||||||
|
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||||
|
|
||||||
|
# Project management
|
||||||
|
openspec init [path] # Initialize OpenSpec
|
||||||
|
openspec update [path] # Update instruction files
|
||||||
|
|
||||||
|
# Interactive mode
|
||||||
|
openspec show # Prompts for selection
|
||||||
|
openspec validate # Bulk validation mode
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
openspec show [change] --json --deltas-only
|
||||||
|
openspec validate [change] --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Flags
|
||||||
|
|
||||||
|
- `--json` - Machine-readable output
|
||||||
|
- `--type change|spec` - Disambiguate items
|
||||||
|
- `--strict` - Comprehensive validation
|
||||||
|
- `--no-interactive` - Disable prompts
|
||||||
|
- `--skip-specs` - Archive without spec updates
|
||||||
|
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/
|
||||||
|
├── project.md # Project conventions
|
||||||
|
├── specs/ # Current truth - what IS built
|
||||||
|
│ └── [capability]/ # Single focused capability
|
||||||
|
│ ├── spec.md # Requirements and scenarios
|
||||||
|
│ └── design.md # Technical patterns
|
||||||
|
├── changes/ # Proposals - what SHOULD change
|
||||||
|
│ ├── [change-name]/
|
||||||
|
│ │ ├── proposal.md # Why, what, impact
|
||||||
|
│ │ ├── tasks.md # Implementation checklist
|
||||||
|
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||||
|
│ │ └── specs/ # Delta changes
|
||||||
|
│ │ └── [capability]/
|
||||||
|
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||||
|
│ └── archive/ # Completed changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Change Proposals
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
New request?
|
||||||
|
├─ Bug fix restoring spec behavior? → Fix directly
|
||||||
|
├─ Typo/format/comment? → Fix directly
|
||||||
|
├─ New feature/capability? → Create proposal
|
||||||
|
├─ Breaking change? → Create proposal
|
||||||
|
├─ Architecture change? → Create proposal
|
||||||
|
└─ Unclear? → Create proposal (safer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposal Structure
|
||||||
|
|
||||||
|
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||||
|
|
||||||
|
2. **Write proposal.md:**
|
||||||
|
```markdown
|
||||||
|
## Why
|
||||||
|
[1-2 sentences on problem/opportunity]
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- [Bullet list of changes]
|
||||||
|
- [Mark breaking changes with **BREAKING**]
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: [list capabilities]
|
||||||
|
- Affected code: [key files/systems]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: New Feature
|
||||||
|
The system SHALL provide...
|
||||||
|
|
||||||
|
#### Scenario: Success case
|
||||||
|
- **WHEN** user performs action
|
||||||
|
- **THEN** expected result
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
### Requirement: Existing Feature
|
||||||
|
[Complete modified requirement]
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
### Requirement: Old Feature
|
||||||
|
**Reason**: [Why removing]
|
||||||
|
**Migration**: [How to handle]
|
||||||
|
```
|
||||||
|
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||||
|
|
||||||
|
4. **Create tasks.md:**
|
||||||
|
```markdown
|
||||||
|
## 1. Implementation
|
||||||
|
- [ ] 1.1 Create database schema
|
||||||
|
- [ ] 1.2 Implement API endpoint
|
||||||
|
- [ ] 1.3 Add frontend component
|
||||||
|
- [ ] 1.4 Write tests
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create design.md when needed:**
|
||||||
|
Create `design.md` if any of the following apply; otherwise omit it:
|
||||||
|
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||||
|
- New external dependency or significant data model changes
|
||||||
|
- Security, performance, or migration complexity
|
||||||
|
- Ambiguity that benefits from technical decisions before coding
|
||||||
|
|
||||||
|
Minimal `design.md` skeleton:
|
||||||
|
```markdown
|
||||||
|
## Context
|
||||||
|
[Background, constraints, stakeholders]
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
- Goals: [...]
|
||||||
|
- Non-Goals: [...]
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Decision: [What and why]
|
||||||
|
- Alternatives considered: [Options + rationale]
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
- [Risk] → Mitigation
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
[Steps, rollback]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spec File Format
|
||||||
|
|
||||||
|
### Critical: Scenario Formatting
|
||||||
|
|
||||||
|
**CORRECT** (use #### headers):
|
||||||
|
```markdown
|
||||||
|
#### Scenario: User login success
|
||||||
|
- **WHEN** valid credentials provided
|
||||||
|
- **THEN** return JWT token
|
||||||
|
```
|
||||||
|
|
||||||
|
**WRONG** (don't use bullets or bold):
|
||||||
|
```markdown
|
||||||
|
- **Scenario: User login** ❌
|
||||||
|
**Scenario**: User login ❌
|
||||||
|
### Scenario: User login ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
Every requirement MUST have at least one scenario.
|
||||||
|
|
||||||
|
### Requirement Wording
|
||||||
|
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||||
|
|
||||||
|
### Delta Operations
|
||||||
|
|
||||||
|
- `## ADDED Requirements` - New capabilities
|
||||||
|
- `## MODIFIED Requirements` - Changed behavior
|
||||||
|
- `## REMOVED Requirements` - Deprecated features
|
||||||
|
- `## RENAMED Requirements` - Name changes
|
||||||
|
|
||||||
|
Headers matched with `trim(header)` - whitespace ignored.
|
||||||
|
|
||||||
|
#### When to use ADDED vs MODIFIED
|
||||||
|
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||||
|
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||||
|
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||||
|
|
||||||
|
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||||
|
|
||||||
|
Authoring a MODIFIED requirement correctly:
|
||||||
|
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||||
|
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||||
|
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||||
|
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||||
|
|
||||||
|
Example for RENAMED:
|
||||||
|
```markdown
|
||||||
|
## RENAMED Requirements
|
||||||
|
- FROM: `### Requirement: Login`
|
||||||
|
- TO: `### Requirement: User Authentication`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
**"Change must have at least one delta"**
|
||||||
|
- Check `changes/[name]/specs/` exists with .md files
|
||||||
|
- Verify files have operation prefixes (## ADDED Requirements)
|
||||||
|
|
||||||
|
**"Requirement must have at least one scenario"**
|
||||||
|
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||||
|
- Don't use bullet points or bold for scenario headers
|
||||||
|
|
||||||
|
**Silent scenario parsing failures**
|
||||||
|
- Exact format required: `#### Scenario: Name`
|
||||||
|
- Debug with: `openspec show [change] --json --deltas-only`
|
||||||
|
|
||||||
|
### Validation Tips
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always use strict mode for comprehensive checks
|
||||||
|
openspec validate [change] --strict
|
||||||
|
|
||||||
|
# Debug delta parsing
|
||||||
|
openspec show [change] --json | jq '.deltas'
|
||||||
|
|
||||||
|
# Check specific requirement
|
||||||
|
openspec show [spec] --json -r 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Happy Path Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Explore current state
|
||||||
|
openspec spec list --long
|
||||||
|
openspec list
|
||||||
|
# Optional full-text search:
|
||||||
|
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||||
|
# rg -n "^#|Requirement:" openspec/changes
|
||||||
|
|
||||||
|
# 2) Choose change id and scaffold
|
||||||
|
CHANGE=add-two-factor-auth
|
||||||
|
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||||
|
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||||
|
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||||
|
|
||||||
|
# 3) Add deltas (example)
|
||||||
|
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
Users MUST provide a second factor during login.
|
||||||
|
|
||||||
|
#### Scenario: OTP required
|
||||||
|
- **WHEN** valid credentials are provided
|
||||||
|
- **THEN** an OTP challenge is required
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4) Validate
|
||||||
|
openspec validate $CHANGE --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Capability Example
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/changes/add-2fa-notify/
|
||||||
|
├── proposal.md
|
||||||
|
├── tasks.md
|
||||||
|
└── specs/
|
||||||
|
├── auth/
|
||||||
|
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||||
|
└── notifications/
|
||||||
|
└── spec.md # ADDED: OTP email notification
|
||||||
|
```
|
||||||
|
|
||||||
|
auth/spec.md
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
notifications/spec.md
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: OTP Email Notification
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Simplicity First
|
||||||
|
- Default to <100 lines of new code
|
||||||
|
- Single-file implementations until proven insufficient
|
||||||
|
- Avoid frameworks without clear justification
|
||||||
|
- Choose boring, proven patterns
|
||||||
|
|
||||||
|
### Complexity Triggers
|
||||||
|
Only add complexity with:
|
||||||
|
- Performance data showing current solution too slow
|
||||||
|
- Concrete scale requirements (>1000 users, >100MB data)
|
||||||
|
- Multiple proven use cases requiring abstraction
|
||||||
|
|
||||||
|
### Clear References
|
||||||
|
- Use `file.ts:42` format for code locations
|
||||||
|
- Reference specs as `specs/auth/spec.md`
|
||||||
|
- Link related changes and PRs
|
||||||
|
|
||||||
|
### Capability Naming
|
||||||
|
- Use verb-noun: `user-auth`, `payment-capture`
|
||||||
|
- Single purpose per capability
|
||||||
|
- 10-minute understandability rule
|
||||||
|
- Split if description needs "AND"
|
||||||
|
|
||||||
|
### Change ID Naming
|
||||||
|
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||||
|
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||||
|
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||||
|
|
||||||
|
## Tool Selection Guide
|
||||||
|
|
||||||
|
| Task | Tool | Why |
|
||||||
|
|------|------|-----|
|
||||||
|
| Find files by pattern | Glob | Fast pattern matching |
|
||||||
|
| Search code content | Grep | Optimized regex search |
|
||||||
|
| Read specific files | Read | Direct file access |
|
||||||
|
| Explore unknown scope | Task | Multi-step investigation |
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
|
||||||
|
### Change Conflicts
|
||||||
|
1. Run `openspec list` to see active changes
|
||||||
|
2. Check for overlapping specs
|
||||||
|
3. Coordinate with change owners
|
||||||
|
4. Consider combining proposals
|
||||||
|
|
||||||
|
### Validation Failures
|
||||||
|
1. Run with `--strict` flag
|
||||||
|
2. Check JSON output for details
|
||||||
|
3. Verify spec file format
|
||||||
|
4. Ensure scenarios properly formatted
|
||||||
|
|
||||||
|
### Missing Context
|
||||||
|
1. Read project.md first
|
||||||
|
2. Check related specs
|
||||||
|
3. Review recent archives
|
||||||
|
4. Ask for clarification
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Stage Indicators
|
||||||
|
- `changes/` - Proposed, not yet built
|
||||||
|
- `specs/` - Built and deployed
|
||||||
|
- `archive/` - Completed changes
|
||||||
|
|
||||||
|
### File Purposes
|
||||||
|
- `proposal.md` - Why and what
|
||||||
|
- `tasks.md` - Implementation steps
|
||||||
|
- `design.md` - Technical decisions
|
||||||
|
- `spec.md` - Requirements and behavior
|
||||||
|
|
||||||
|
### CLI Essentials
|
||||||
|
```bash
|
||||||
|
openspec list # What's in progress?
|
||||||
|
openspec show [item] # View details
|
||||||
|
openspec validate --strict # Is it correct?
|
||||||
|
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
当前 API 服务基于 Gin 构建。本次任务的目标是将其完整迁移到 Echo 框架,同时保持功能和接口的完全向后兼容。这包括路由、请求处理、中间件、Swagger 文档和 pprof 分析工具。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- **Goals**:
|
||||||
|
- 成功将 Web 框架从 Gin 迁移到 Echo v4。
|
||||||
|
- 保持所有现有 API 端点的路径、方法和行为不变。
|
||||||
|
- 确保所有自定义中间件(认证、审计日志)功能正常。
|
||||||
|
- 确保 Swagger UI 可以在 `/swagger/index.html` 正常访问。
|
||||||
|
- 确保 pprof 调试端点在 `/debug/pprof/*` 路径下正常工作。
|
||||||
|
- **Non-Goals**:
|
||||||
|
- 增加任何新的 API 端点或功能。
|
||||||
|
- 修改任何现有的 API 请求/响应模型。
|
||||||
|
- 在本次变更中引入新的业务逻辑。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
以下是从 Gin 到 Echo 的关键组件映射决策:
|
||||||
|
|
||||||
|
1. **框架实例**:
|
||||||
|
- **From**: `gin.SetMode(cfg.Mode)`, `engine := gin.New()`, `engine.Use(gin.Recovery())`
|
||||||
|
- **To**: `e := echo.New()`, `e.Debug = (cfg.Mode == "debug")`, `e.Use(middleware.Recover())`
|
||||||
|
- **Rationale**: `echo.New()` 提供了干净的实例。Echo 的 `Debug` 属性控制调试模式,可以根据配置设置。Echo 提供了内置的 `middleware.Recover()` 来替代 Gin 的 Recovery 中间件。
|
||||||
|
|
||||||
|
2. **上下文对象 (Context) 与处理器签名**:
|
||||||
|
- **From**: `func(c *gin.Context)`
|
||||||
|
- **To**: `func(c echo.Context) error`
|
||||||
|
- **Rationale**: 这是两个框架的核心区别。所有控制器处理函数签名都需要更新。常见方法映射如下:
|
||||||
|
- `ctx.ShouldBindJSON(&req)` -> `c.Bind(&req)` (Echo 的 `Bind` 更通用)
|
||||||
|
- `ctx.Param("id")` -> `c.Param("id")`
|
||||||
|
- `ctx.GetHeader("Authorization")` -> `c.Request().Header.Get("Authorization")`
|
||||||
|
- `ctx.Set/Get("key", value)` -> `c.Set/Get("key")`
|
||||||
|
- `ctx.ClientIP()` -> `c.RealIP()`
|
||||||
|
- `controller.SendResponse(ctx, ...)` -> `return controller.SendResponse(c, ...)`
|
||||||
|
- `ctx.AbortWithStatusJSON(...)` -> 对于需要返回特定HTTP状态码的场景(如认证中间件),将使用一个专门的辅助函数 `return controller.SendErrorWithStatus(c, http.StatusUnauthorized, ...)`。
|
||||||
|
|
||||||
|
3. **中间件 (Middleware)**:
|
||||||
|
- **From**: `func AuthMiddleware(...) gin.HandlerFunc { return func(c *gin.Context) { ... } }`
|
||||||
|
- **To**: `func AuthMiddleware(...) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ...; return next(c) } } }`
|
||||||
|
- **Rationale**: Echo 的中间件是一个包装器模式。我们需要将现有的 `AuthMiddleware` 和 `AuditLogMiddleware` 逻辑迁移到这个新的结构中。
|
||||||
|
|
||||||
|
4. **Swagger 集成**:
|
||||||
|
- **From**: `github.com/swaggo/gin-swagger`
|
||||||
|
- **To**: `github.com/swaggo/echo-swagger`
|
||||||
|
- **Rationale**: 这是 `swaggo` 官方为 Echo 提供的适配库,可以无缝替换。
|
||||||
|
|
||||||
|
5. **Pprof 与其他 `net/http` 处理器集成**:
|
||||||
|
- **From**: `gin.WrapH` 和 `gin.WrapF`
|
||||||
|
- **To**: `echo.WrapHandler` 和 `echo.WrapFunc`
|
||||||
|
- **Rationale**: Echo 提供了类似的 `net/http` 处理器包装函数。
|
||||||
|
|
||||||
|
6. **控制器辅助函数与审计逻辑重构**:
|
||||||
|
- **Affected Files**: `response.go`, `auth_utils.go`, `controller_helpers.go`
|
||||||
|
- **Change**:
|
||||||
|
- 所有辅助函数中的 `*gin.Context` 都将替换为 `echo.Context`。
|
||||||
|
- **`response.go` 将被重构**:`setAuditDetails` 函数将成为设置所有审计信息(包括操作状态和失败详情)的唯一入口。`SendSuccessWithAudit` 和 `SendErrorWithAudit` 会调用它来将最终结果存入 `echo.Context`。
|
||||||
|
- `controller_helpers.go` 中的泛型辅助函数将修改为返回 `error`,以适配 Echo 的错误处理链。
|
||||||
|
- **Rationale**: 这种重构使得审计逻辑更加清晰和内聚,避免了在中间件中进行复杂的响应体捕获。
|
||||||
|
|
||||||
|
7. **DTO 注解 (Annotations)**:
|
||||||
|
- **From**: Gin 相关的注解,主要包括 `binding:"..."` 和 `form:"..."`。
|
||||||
|
- **To**: Echo 兼容的注解,主要包括 `validate:"..."` 和 `query:"..."`。
|
||||||
|
- **Rationale**: Gin 使用 `binding` 标签进行请求参数绑定和验证,`form` 标签用于表单或查询参数绑定。Echo 框架通常结合 `go-playground/validator` 库进行验证,其对应的标签为 `validate`。对于查询参数,Echo 默认使用 `query` 标签。
|
||||||
|
- **通用修改规则**:
|
||||||
|
- `json:"..."` 标签保持不变。
|
||||||
|
- `example:"..."` 标签保持不变。
|
||||||
|
- 将 `binding:"required"` 替换为 `validate:"required"`。
|
||||||
|
- 将 `form:"field,default=value"` 替换为 `query:"field"`。`default` 行为需在代码中手动实现(如在 DTO 构造函数中设置默认值),标签中不再需要。
|
||||||
|
- 将 `form:"field"` 替换为 `query:"field"`。
|
||||||
|
- 对于 `json:"...,omitempty"` 的字段,在 `validate` 标签中也添加 `omitempty`。
|
||||||
|
- 对于结构体切片或数组字段,在 `validate` 标签中添加 `dive` 以递归验证切片元素。
|
||||||
|
- 根据字段的业务含义,添加更具体的 `validate` 规则(例如 `min=0`, `cron` 等)。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Risk**: 迁移工作量大,可能遗漏某些 Gin 特有的功能或上下文用法,导致运行时错误。
|
||||||
|
- **Mitigation**: 采用逐个文件、逐个控制器修改的方式,每修改完一部分就进行编译检查。在完成所有编码后,进行全面的手动 API 测试。
|
||||||
|
- **Risk (Resolved)**: `AuditLogMiddleware` 中间件最初的设计依赖于捕获响应体,这在 Echo 中难以实现。
|
||||||
|
- **Resolution**: 我们通过重构 `response.go` 解决了这个问题。现在,控制器在调用响应函数时,会将最终的操作状态(成功/失败)和结果详情直接存入 `echo.Context`。`AuditLogMiddleware` 只需从上下文中读取这些信息即可,**完全消除了捕获和解析响应体的需要**,使得设计更加清晰和高效。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
本项目当前使用 Gin 作为核心 Web 框架。Gin 的路由系统存在一些限制,例如无法优雅地支持类似 `/:id/action` 和 `/:other_id/other-action` 这种在同一层级使用不同动态参数的路由模式。为了解决此问题并利用更现代、灵活的路由和中间件系统,我们计划将框架迁移到 Echo (v4)。本次变更仅进行框架替换,暂不修改现有路由结构。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **核心框架替换**: 将 `github.com/gin-gonic/gin` 的所有引用替换为 `github.com/labstack/echo/v4`。
|
||||||
|
- **API 路由重写**: 更新 `internal/app/api/router.go` 以使用 Echo 的路由注册方式。
|
||||||
|
- **上下文对象适配**: 在所有 Controller 和 Middleware 中,将 `*gin.Context` 替换为 `echo.Context`,并调整相关方法调用。
|
||||||
|
- **中间件迁移**: 将现有的 Gin 中间件 (`AuthMiddleware`, `AuditLogMiddleware`) 适配为 Echo 的中间件格式。
|
||||||
|
- **Swagger 文档适配**: 将 `gin-swagger` 替换为 Echo 兼容的 `echo-swagger`,确保 API 文档能够正常生成和访问。
|
||||||
|
- **Pprof 路由适配**: 确保性能分析工具 pprof 的路由在 Echo 框架下正常工作。
|
||||||
|
|
||||||
|
**BREAKING**: 这是一项纯粹的技术栈重构,**不应该**对外部 API 消费者产生任何破坏性影响。所有 API 端点、请求/响应格式将保持完全兼容。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Affected specs**: 无。此变更是技术实现层面的重构,不改变任何已定义的功能规约。
|
||||||
|
- **Affected code**:
|
||||||
|
- `go.mod` / `go.sum`: 依赖项变更。
|
||||||
|
- `config.yml` / `config.example.yml`: 更新 `mode` 配置项的注释。
|
||||||
|
- `internal/app/api/api.go`
|
||||||
|
- `internal/app/api/router.go`
|
||||||
|
- `internal/app/middleware/auth.go`
|
||||||
|
- `internal/app/middleware/audit.go`
|
||||||
|
- `internal/app/controller/**/*.go`: 所有控制器及其辅助函数。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# HTTP Server Specification
|
||||||
|
|
||||||
|
本文档概述了 HTTP 服务器的需求。
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: API 服务器框架已更新
|
||||||
|
|
||||||
|
- **说明**: 底层 Web 框架从 Gin 迁移到 Echo。所有现有的 API 端点 **MUST** 保持功能齐全和向后兼容。
|
||||||
|
- **理由**: 为了提高路由灵活性并使技术栈现代化。这是一次技术重构,不会改变任何外部 API 行为。
|
||||||
|
- **影响**: 高。影响核心请求处理、路由和中间件。
|
||||||
|
- **受影响的端点**: 全部。
|
||||||
|
|
||||||
|
#### Scenario: 所有现有的 API 端点保持功能齐全和向后兼容
|
||||||
|
- **假如**: API 服务器在迁移到 Echo 后正在运行。
|
||||||
|
- **当**: 客户端向任何现有的 API 端点(例如, `POST /api/v1/users/login`)发送请求。
|
||||||
|
- **那么**: 服务器处理该请求并返回与使用 Gin 框架时完全相同的响应(状态码、头部和正文格式)。
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
## 任务清单:Gin 到 Echo 迁移
|
||||||
|
|
||||||
|
- [x] **1. 配置文件 (无代码依赖)**
|
||||||
|
- [x] 修改 `config.yml` 中 `mode` 配置项的注释,将 "Gin 运行模式" 改为 "服务运行模式"。
|
||||||
|
- [x] 修改 `config.example.yml` 中 `mode` 配置项的注释,保持与 `config.yml` 一致。
|
||||||
|
|
||||||
|
- [x] **2. 控制器辅助函数 (最基础的依赖)**
|
||||||
|
- [x] **`internal/infra/models/execution.go`**
|
||||||
|
- [x] 添加 `ContextAuditStatus` 和 `ContextAuditResultDetails` 常量。
|
||||||
|
- [x] **`internal/app/controller/response.go`**
|
||||||
|
- [x] 将 `*gin.Context` 参数全部替换为 `echo.Context`。
|
||||||
|
- [x] 修改响应函数,使其返回 `error`。
|
||||||
|
- [x] **新增 `SendErrorWithStatus` 函数**,用于在中间件等场景下发送带有特定HTTP状态码的错误响应。
|
||||||
|
- [x] **重构 `setAuditDetails` 函数**,使其成为统一设置所有审计信息(包括操作状态和失败详情)的唯一入口。
|
||||||
|
- [x] 更新 `SendSuccessWithAudit` 和 `SendErrorWithAudit` 以调用重构后的 `setAuditDetails`。
|
||||||
|
- [x] **`internal/app/controller/auth_utils.go`**
|
||||||
|
- [x] 将 `*gin.Context` 参数全部替换为 `echo.Context`。
|
||||||
|
- [x] 适配 `Get...FromContext` 系列函数,使用 `c.Get("key")` 提取数据。
|
||||||
|
|
||||||
|
- [x] **3. 中间件 (`internal/app/middleware`)**
|
||||||
|
- [x] **`auth.go`**
|
||||||
|
- [x] 迁移到 Echo 中间件格式。
|
||||||
|
- [x] **使用 `controller.SendErrorWithStatus`** 在认证失败时返回 `401` 或 `500` HTTP状态码。
|
||||||
|
- [x] **`audit.go`**
|
||||||
|
- [x] **极大简化并迁移到 Echo 中间件格式**。
|
||||||
|
- [x] **移除所有响应体捕获和解析的逻辑** (`bodyLogWriter`, `auditResponse` 等)。
|
||||||
|
- [x] 在 `next(c)` 调用后,**直接从 `echo.Context` 中获取**由 `response.go` 设置好的最终审计状态和结果详情。
|
||||||
|
|
||||||
|
- [x] **4. 控制器 (`internal/app/controller/...`)**
|
||||||
|
- [x] **通用修改**:对所有控制器文件执行以下操作:
|
||||||
|
- [x] 将 `import "github.com/gin-gonic/gin"` 替换为 `import "github.com/labstack/echo/v4"`。
|
||||||
|
- [x] 将所有处理函数签名从 `func(c *gin.Context)` 修改为 `func(c echo.Context) error`。
|
||||||
|
- [x] 将 `c.ShouldBindJSON(&req)` 或 `c.ShouldBindQuery(&req)` 替换为
|
||||||
|
`if err := c.Bind(&req); err != nil { ... }`。
|
||||||
|
- [x] 将 `c.Param("id")` 替换为 `c.Param("id")` (用法相同,检查返回值即可)。
|
||||||
|
- [x] 将 `controller.SendResponse(c, ...)` 和 `controller.SendErrorResponse(c, ...)` 调用修改为
|
||||||
|
`return controller.SendResponse(c, ...)` 和 `return controller.SendErrorResponse(c, ...)`。
|
||||||
|
- [x] **文件清单** (按依赖顺序建议):
|
||||||
|
- [x] `internal/app/controller/management/controller_helpers.go` (注意:其中的泛型辅助函数也需要修改为返回
|
||||||
|
`error`)
|
||||||
|
- [x] `internal/app/controller/device/device_controller.go`
|
||||||
|
- [x] `internal/app/controller/management/pig_farm_controller.go`
|
||||||
|
- [x] `internal/app/controller/management/pig_batch_controller.go`
|
||||||
|
- [x] `internal/app/controller/management/pig_batch_health_controller.go`
|
||||||
|
- [x] `internal/app/controller/management/pig_batch_trade_controller.go`
|
||||||
|
- [x] `internal/app/controller/management/pig_batch_transfer_controller.go`
|
||||||
|
- [x] `internal/app/controller/monitor/monitor_controller.go`
|
||||||
|
- [x] `internal/app/controller/plan/plan_controller.go`
|
||||||
|
- [x] `internal/app/controller/user/user_controller.go`
|
||||||
|
|
||||||
|
- [x] **5. DTO 结构体注解**
|
||||||
|
- [x] **通用修改规则**:
|
||||||
|
- [x] `json:"..."` 标签保持不变。
|
||||||
|
- [x] `example:"..."` 标签保持不变。
|
||||||
|
- [x] 将 `binding:"required"` 替换为 `validate:"required"`。
|
||||||
|
- [x] 将 `form:"field,default=value"` 替换为 `query:"field"`。`default` 行为需在代码中手动实现(如在 DTO 构造函数中设置默认值),标签中不再需要。
|
||||||
|
- [x] 将 `form:"field"` 替换为 `query:"field"`。
|
||||||
|
- [x] 对于 `json:"...,omitempty"` 的字段,在 `validate` 标签中也添加 `omitempty`。
|
||||||
|
- [x] 对于结构体切片或数组字段,在 `validate` 标签中添加 `dive` 以递归验证切片元素。
|
||||||
|
- [x] 根据字段的业务含义,添加更具体的 `validate` 规则(例如 `min=0`, `cron` 等)。
|
||||||
|
|
||||||
|
- [x] **文件清单** (按 `internal/app/dto` 目录下的文件顺序):
|
||||||
|
- [x] `internal/app/dto/plan_dto.go`
|
||||||
|
- [x] `ListPlansQuery.PlanType`: `form:"planType,default=自定义任务"` -> `query:"planType"`
|
||||||
|
- [x] `ListPlansQuery.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPlansQuery.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `CreatePlanRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreatePlanRequest.ExecutionType`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreatePlanRequest.ExecuteNum`: 添加 `validate:"omitempty,min=0"`
|
||||||
|
- [x] `CreatePlanRequest.CronExpression`: 添加 `validate:"omitempty,cron"`
|
||||||
|
- [x] `CreatePlanRequest.SubPlanIDs`: 添加 `validate:"omitempty,dive"`
|
||||||
|
- [x] `CreatePlanRequest.Tasks`: 添加 `validate:"omitempty,dive"`
|
||||||
|
- [x] `UpdatePlanRequest.ExecutionType`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePlanRequest.ExecuteNum`: 添加 `validate:"omitempty,min=0"`
|
||||||
|
- [x] `UpdatePlanRequest.CronExpression`: 添加 `validate:"omitempty,cron"`
|
||||||
|
- [x] `UpdatePlanRequest.SubPlanIDs`: 添加 `validate:"omitempty,dive"`
|
||||||
|
- [x] `UpdatePlanRequest.Tasks`: 添加 `validate:"omitempty,dive"`
|
||||||
|
- [x] `internal/app/dto/user_dto.go`
|
||||||
|
- [x] `CreateUserRequest.Username`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateUserRequest.Password`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `LoginRequest.Identifier`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `LoginRequest.Password`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `internal/app/dto/device_dto.go`
|
||||||
|
- [x] `CreateDeviceRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceRequest.DeviceTemplateID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceRequest.AreaControllerID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateDeviceRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateDeviceRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceRequest.DeviceTemplateID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceRequest.AreaControllerID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateDeviceRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateAreaControllerRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateAreaControllerRequest.NetworkID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateAreaControllerRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateAreaControllerRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateAreaControllerRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateAreaControllerRequest.NetworkID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateAreaControllerRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateAreaControllerRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Manufacturer`: `json:"manufacturer,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Description`: `json:"description,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Category`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Commands`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreateDeviceTemplateRequest.Values`: `json:"values,omitempty"` -> `validate:"omitempty,dive"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Manufacturer`: `json:"manufacturer,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Description`: `json:"description,omitempty"` -> `validate:"omitempty"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Category`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Commands`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdateDeviceTemplateRequest.Values`: `json:"values,omitempty"` -> `validate:"omitempty,dive"`
|
||||||
|
- [x] `internal/app/dto/monitor_dto.go`
|
||||||
|
- [x] `ListSensorDataRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListSensorDataRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListSensorDataRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
|
||||||
|
- [x] `ListSensorDataRequest.SensorType`: `form:"sensor_type"` -> `query:"sensor_type"`
|
||||||
|
- [x] `ListSensorDataRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListSensorDataRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListSensorDataRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.ReceivedSuccess`: `form:"received_success"` -> `query:"received_success"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListDeviceCommandLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.PlanID`: `form:"plan_id"` -> `query:"plan_id"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.Status`: `form:"status"` -> `query:"status"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPlanExecutionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.PlanExecutionLogID`: `form:"plan_execution_log_id"` -> `query:"plan_execution_log_id"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.TaskID`: `form:"task_id"` -> `query:"task_id"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.Status`: `form:"status"` -> `query:"status"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListTaskExecutionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPendingCollectionRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPendingCollectionRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPendingCollectionRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
|
||||||
|
- [x] `ListPendingCollectionRequest.Status`: `form:"status"` -> `query:"status"`
|
||||||
|
- [x] `ListPendingCollectionRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPendingCollectionRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPendingCollectionRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListUserActionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListUserActionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListUserActionLogRequest.UserID`: `form:"user_id"` -> `query:"user_id"`
|
||||||
|
- [x] `ListUserActionLogRequest.Username`: `form:"username"` -> `query:"username"`
|
||||||
|
- [x] `ListUserActionLogRequest.ActionType`: `form:"action_type"` -> `query:"action_type"`
|
||||||
|
- [x] `ListUserActionLogRequest.Status`: `form:"status"` -> `query:"status"`
|
||||||
|
- [x] `ListUserActionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListUserActionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListUserActionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.RawMaterialID`: `form:"raw_material_id"` -> `query:"raw_material_id"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.Supplier`: `form:"supplier"` -> `query:"supplier"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListRawMaterialPurchaseRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.RawMaterialID`: `form:"raw_material_id"` -> `query:"raw_material_id"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.SourceType`: `form:"source_type"` -> `query:"source_type"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.SourceID`: `form:"source_id"` -> `query:"source_id"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListRawMaterialStockLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.FeedFormulaID`: `form:"feed_formula_id"` -> `query:"feed_formula_id"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListFeedUsageRecordRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListMedicationLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListMedicationLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListMedicationLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListMedicationLogRequest.MedicationID`: `form:"medication_id"` -> `query:"medication_id"`
|
||||||
|
- [x] `ListMedicationLogRequest.Reason`: `form:"reason"` -> `query:"reason"`
|
||||||
|
- [x] `ListMedicationLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListMedicationLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListMedicationLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListMedicationLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPigBatchLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPigBatchLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPigBatchLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListPigBatchLogRequest.ChangeType`: `form:"change_type"` -> `query:"change_type"`
|
||||||
|
- [x] `ListPigBatchLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListPigBatchLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPigBatchLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPigBatchLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListWeighingBatchRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListWeighingBatchRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListWeighingBatchRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListWeighingBatchRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListWeighingBatchRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListWeighingBatchRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListWeighingRecordRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListWeighingRecordRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListWeighingRecordRequest.WeighingBatchID`: `form:"weighing_batch_id"` -> `query:"weighing_batch_id"`
|
||||||
|
- [x] `ListWeighingRecordRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
|
||||||
|
- [x] `ListWeighingRecordRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListWeighingRecordRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListWeighingRecordRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListWeighingRecordRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPigTransferLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPigTransferLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPigTransferLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListPigTransferLogRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
|
||||||
|
- [x] `ListPigTransferLogRequest.TransferType`: `form:"transfer_type"` -> `query:"transfer_type"`
|
||||||
|
- [x] `ListPigTransferLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListPigTransferLogRequest.CorrelationID`: `form:"correlation_id"` -> `query:"correlation_id"`
|
||||||
|
- [x] `ListPigTransferLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPigTransferLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPigTransferLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPigSickLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPigSickLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPigSickLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListPigSickLogRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
|
||||||
|
- [x] `ListPigSickLogRequest.Reason`: `form:"reason"` -> `query:"reason"`
|
||||||
|
- [x] `ListPigSickLogRequest.TreatmentLocation`: `form:"treatment_location"` -> `query:"treatment_location"`
|
||||||
|
- [x] `ListPigSickLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListPigSickLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPigSickLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPigSickLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPigPurchaseRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPigPurchaseRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPigPurchaseRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListPigPurchaseRequest.Supplier`: `form:"supplier"` -> `query:"supplier"`
|
||||||
|
- [x] `ListPigPurchaseRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListPigPurchaseRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPigPurchaseRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPigPurchaseRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `ListPigSaleRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListPigSaleRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListPigSaleRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
|
||||||
|
- [x] `ListPigSaleRequest.Buyer`: `form:"buyer"` -> `query:"buyer"`
|
||||||
|
- [x] `ListPigSaleRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
|
||||||
|
- [x] `ListPigSaleRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListPigSaleRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListPigSaleRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `internal/app/dto/pig_farm_dto.go`
|
||||||
|
- [x] `CreatePigHouseRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePigHouseRequest.Name`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreatePenRequest.PenNumber`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreatePenRequest.HouseID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `CreatePenRequest.Capacity`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePenRequest.PenNumber`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePenRequest.HouseID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePenRequest.Capacity`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `UpdatePenRequest.Status`: `binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` -> `validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"`
|
||||||
|
- [x] `UpdatePenStatusRequest.Status`: `binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` -> `validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"`
|
||||||
|
- [x] `internal/app/dto/pig_batch_dto.go`
|
||||||
|
- [x] `PigBatchCreateDTO.BatchNumber`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `PigBatchCreateDTO.OriginType`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `PigBatchCreateDTO.StartDate`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `PigBatchCreateDTO.InitialCount`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `PigBatchCreateDTO.Status`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `PigBatchQueryDTO.IsActive`: `form:"is_active"` -> `query:"is_active"`
|
||||||
|
- [x] `AssignEmptyPensToBatchRequest.PenIDs`: `binding:"required,min=1"` -> `validate:"required,min=1,dive"`
|
||||||
|
- [x] `ReclassifyPenToNewBatchRequest.ToBatchID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `ReclassifyPenToNewBatchRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RemoveEmptyPenFromBatchRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `MovePigsIntoPenRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `MovePigsIntoPenRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `SellPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `SellPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `SellPigsRequest.UnitPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
|
||||||
|
- [x] `SellPigsRequest.TotalPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
|
||||||
|
- [x] `SellPigsRequest.TraderName`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `SellPigsRequest.TradeDate`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `BuyPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `BuyPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `BuyPigsRequest.UnitPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
|
||||||
|
- [x] `BuyPigsRequest.TotalPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
|
||||||
|
- [x] `BuyPigsRequest.TraderName`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `BuyPigsRequest.TradeDate`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsAcrossBatchesRequest.DestBatchID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsAcrossBatchesRequest.FromPenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsAcrossBatchesRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsAcrossBatchesRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `TransferPigsWithinBatchRequest.FromPenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsWithinBatchRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `TransferPigsWithinBatchRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordSickPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordSickPigsRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigsRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigRecoveryRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigRecoveryRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordSickPigRecoveryRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigRecoveryRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigDeathRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigDeathRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordSickPigDeathRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigDeathRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigCullRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigCullRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordSickPigCullRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordSickPigCullRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordDeathRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordDeathRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordDeathRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordCullRequest.PenID`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `RecordCullRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
|
||||||
|
- [x] `RecordCullRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `internal/app/dto/notification_dto.go`
|
||||||
|
- [x] `SendTestNotificationRequest.Type`: `binding:"required"` -> `validate:"required"`
|
||||||
|
- [x] `ListNotificationRequest.Page`: `form:"page,default=1"` -> `query:"page"`
|
||||||
|
- [x] `ListNotificationRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
|
||||||
|
- [x] `ListNotificationRequest.UserID`: `form:"user_id"` -> `query:"user_id"`
|
||||||
|
- [x] `ListNotificationRequest.NotifierType`: `form:"notifier_type"` -> `query:"notifier_type"`
|
||||||
|
- [x] `ListNotificationRequest.Status`: `form:"status"` -> `query:"status"`
|
||||||
|
- [x] `ListNotificationRequest.Level`: `form:"level"` -> `query:"level"`
|
||||||
|
- [x] `ListNotificationRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
|
||||||
|
- [x] `ListNotificationRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
|
||||||
|
- [x] `ListNotificationRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
|
||||||
|
- [x] `internal/app/dto/plan_converter.go` (跳过,非 DTO 结构体)
|
||||||
|
- [x] `internal/app/dto/device_converter.go` (跳过,非 DTO 结构体)
|
||||||
|
- [x] `internal/app/dto/monitor_converter.go` (跳过,非 DTO 结构体)
|
||||||
|
- [x] `internal/app/dto/notification_converter.go` (跳过,非 DTO 结构体)
|
||||||
|
|
||||||
|
- [x] **6. 核心 API 层 (`internal/app/api`)**
|
||||||
|
- [x] **`router.go`**
|
||||||
|
- [x] 将所有 `router.GET`, `router.POST` 等 Gin 路由注册方法替换为 Echo 的 `e.GET`, `e.POST` 等方法。
|
||||||
|
- [x] 将 Swagger 路由 `router.GET("/swagger/*", ginSwagger.WrapHandler(swaggerFiles.Handler))` 替换为
|
||||||
|
`e.GET("/swagger/*", echoSwagger.WrapHandler)`。
|
||||||
|
- [x] 将 pprof 路由的 `gin.WrapH` 和 `gin.WrapF` 调用替换为 `echo.WrapHandler` 和 `echo.WrapFunc`。
|
||||||
|
- [x] **`api.go`**
|
||||||
|
- [x] 将 `engine *gin.Engine` 替换为 `engine *echo.Echo`。
|
||||||
|
- [x] 更新 `NewAPI` 函数:
|
||||||
|
- [x] 将 `gin.SetMode(cfg.Mode)` 替换为 `e.Debug = (cfg.Mode == "debug")`。
|
||||||
|
- [x] 将 `gin.New()` 替换为 `echo.New()`。
|
||||||
|
- [x] 将 `engine.Use(middleware.Recover())` 替换为 `e.Use(middleware.Recover())`。
|
||||||
|
|
||||||
|
- [x] **7. 依赖管理**
|
||||||
|
- [x] 在 `go.mod` 中移除 `github.com/gin-gonic/gin`。
|
||||||
|
- [x] 在 `go.mod` 中移除 `github.com/swaggo/gin-swagger`。
|
||||||
|
- [x] 在 `go.mod` 中添加 `github.com/labstack/echo/v4`。
|
||||||
|
- [x] 在 `go.mod` 中添加 `github.com/swaggo/echo-swagger`。
|
||||||
|
- [x] 执行 `go mod tidy` 清理依赖项。
|
||||||
|
|
||||||
|
- [x] **8. 验证**
|
||||||
|
- [x] 运行 `go build ./...` 确保项目能够成功编译。
|
||||||
|
- [x] 启动服务,手动测试所有 API 端点,验证功能是否与迁移前一致。
|
||||||
|
- [x] 访问 `/swagger/index.html`,确认 Swagger UI 是否正常工作。
|
||||||
|
- [x] (可选) 访问 `/debug/pprof/`,确认 pprof 路由是否正常。
|
||||||
31
openspec/project.md
Normal file
31
openspec/project.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Project Context
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
[Describe your project's purpose and goals]
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- [List your primary technologies]
|
||||||
|
- [e.g., TypeScript, React, Node.js]
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
[Describe your code style preferences, formatting rules, and naming conventions]
|
||||||
|
|
||||||
|
### Architecture Patterns
|
||||||
|
[Document your architectural decisions and patterns]
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
[Explain your testing approach and requirements]
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
[Describe your branching strategy and commit conventions]
|
||||||
|
|
||||||
|
## Domain Context
|
||||||
|
[Add domain-specific knowledge that AI assistants need to understand]
|
||||||
|
|
||||||
|
## Important Constraints
|
||||||
|
[List any technical, business, or regulatory constraints]
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
[Document key external services, APIs, or systems]
|
||||||
17
openspec/specs/http-server/spec.md
Normal file
17
openspec/specs/http-server/spec.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# HTTP Server Capability Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
该规范描述了本项目中 HTTP 服务器的功能和设计目标。它确保了 API 的可靠性和可维护性。
|
||||||
|
## Requirements
|
||||||
|
### Requirement: API 服务器框架已更新
|
||||||
|
|
||||||
|
- **说明**: 底层 Web 框架从 Gin 迁移到 Echo。所有现有的 API 端点 **MUST** 保持功能齐全和向后兼容。
|
||||||
|
- **理由**: 为了提高路由灵活性并使技术栈现代化。这是一次技术重构,不会改变任何外部 API 行为。
|
||||||
|
- **影响**: 高。影响核心请求处理、路由和中间件。
|
||||||
|
- **受影响的端点**: 全部。
|
||||||
|
|
||||||
|
#### Scenario: 所有现有的 API 端点保持功能齐全和向后兼容
|
||||||
|
- **假如**: API 服务器在迁移到 Echo 后正在运行。
|
||||||
|
- **当**: 客户端向任何现有的 API 端点(例如, `POST /api/v1/users/login`)发送请求。
|
||||||
|
- **那么**: 服务器处理该请求并返回与使用 Gin 框架时完全相同的响应(状态码、头部和正文格式)。
|
||||||
|
|
||||||
Reference in New Issue
Block a user