Compare commits

...

14 Commits

Author SHA1 Message Date
70fad51f40 Merge pull request 'issue_72' (#73) from issue_72 into main
Reviewed-on: #73
2025-12-02 16:34:47 +08:00
6764684fe7 优化ai初始化逻辑 2025-12-02 16:34:14 +08:00
da2c296c05 去掉无效日志 2025-12-02 16:19:56 +08:00
bdf74652b3 实现ai 2025-12-02 15:51:37 +08:00
70e8627a96 增加ai配置 2025-12-02 13:38:49 +08:00
c2c6577064 格式化 2025-11-29 16:04:12 +08:00
260c7d054c 更新makefile 2025-11-29 15:54:00 +08:00
d25933cf26 Merge pull request 'issue_66' (#70) from issue_66 into main
Reviewed-on: #70
2025-11-29 15:39:49 +08:00
4aa56441ce 归档任务 2025-11-29 15:38:52 +08:00
de68151539 优化日算法 2025-11-28 22:22:39 +08:00
04b46d8025 优化日志 2025-11-28 14:37:53 +08:00
bc4355cad5 修bug 2025-11-27 22:01:37 +08:00
968d996a9b 修bug 2025-11-27 21:47:07 +08:00
d6e5d89768 优化展示 2025-11-27 21:39:09 +08:00
26 changed files with 736 additions and 47 deletions

View File

@@ -3,6 +3,7 @@
1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang 1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang
2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html 2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html
3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构 3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构
4. 项目中有config/presets-data目录, 里面有一些预设数据, 可以间接看作数据库内的数据, 在测试环境中他们一般会和数据库的数据保持一致
# 权限管理 # 权限管理

View File

@@ -10,10 +10,15 @@ help:
@echo " build Build the application" @echo " build Build the application"
@echo " clean Clean generated files" @echo " clean Clean generated files"
@echo " test Run all tests" @echo " test Run all tests"
@echo " swag Generate swagger docs" @echo " swag Generate Swagger docs"
@echo " help Show this help message"
@echo " proto Generate protobuf files" @echo " proto Generate protobuf files"
@echo " lint Lint the code" @echo " lint Lint the code"
@echo " dev Run in development mode with hot-reload"
@echo " mcp-chrome Start the Google Chrome MCP server"
@echo " mcp-pgsql Start the PostgreSQL MCP server"
@echo " tree Generate the project file structure list"
@echo " gemini Start the gemini-cli"
@echo " help Show this help message"
# 运行应用 # 运行应用
.PHONY: run .PHONY: run

View File

@@ -125,3 +125,11 @@ alarm_notification:
dpanic: 1 dpanic: 1
panic: 1 panic: 1
fatal: 1 fatal: 1
# AI 服务配置
ai:
model: "gemini" # 不指定就是不用AI
gemini:
api_key: "YOUR_GEMINI_API_KEY" # 替换为你的 Gemini API Key
model_name: "gemini-2.5-flash" # Gemini 模型名称,例如 "gemini-pro"
timeout: 30 # AI 请求超时时间 (秒)

View File

@@ -103,3 +103,11 @@ alarm_notification:
dpanic: 1 dpanic: 1
panic: 1 panic: 1
fatal: 1 fatal: 1
# AI 服务配置
ai:
model: Gemini
gemini:
api_key: "AIzaSyAJdXUmoN07LIswDac6YxPeRnvXlR73OO8" # 替换为你的 Gemini API Key
model_name: "gemini-2.0-flash" # Gemini 模型名称,例如 "gemini-pro"
timeout: 30 # AI 请求超时时间 (秒)

View File

@@ -46,6 +46,20 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
7. 简单查看功能 7. 简单查看功能
- 两个配方对比页面(营养+成本对比) - 两个配方对比页面(营养+成本对比)
# 实现总结
## 实现内容
实现库存和原料和营养和猪营养需求的管理, 支持根据库存和已录入原料和猪营养需求生成配方
## TODO
1. 发酵料管理考虑到发酵目前没有自动化流程, 不好追踪, 遂暂时不做
2. 目前的价格是根据原料的参考价设置的, 后续应当实现一个在服务供平台采集参考价, 以及使用原料采购价计算
3. 原料应该加上膨润土等, 比如膨润土的黄曲霉素含量应该是负数以表示减少饲料里的含量
4. 饲料保质期考虑到批次间管理暂时不方便, 等可以实现同一原料先进先出后再实现
5. 暂时不支持指定原料列表然后自动生成, 也不支持告诉用户当前生成不出是为什么, 等以后再做
# 完成事项 # 完成事项
1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表

View File

@@ -3371,6 +3371,59 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/feed/recipes/{id}/ai-diagnose": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "使用AI对指定配方进行点评并针对目标猪类型给出建议。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "AI点评配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "猪类型ID",
"name": "pig_type_id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表AI点评成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ReviewRecipeResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/adjust": { "/api/v1/inventory/stock/adjust": {
"post": { "post": {
"security": [ "security": [
@@ -9165,6 +9218,23 @@ const docTemplate = `{
} }
} }
}, },
"dto.ReviewRecipeResponse": {
"type": "object",
"properties": {
"ai_model": {
"description": "使用的 AI 模型",
"allOf": [
{
"$ref": "#/definitions/models.AIModel"
}
]
},
"review_message": {
"description": "点评内容",
"type": "string"
}
}
},
"dto.SellPigsRequest": { "dto.SellPigsRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -10084,6 +10154,15 @@ const docTemplate = `{
} }
} }
}, },
"models.AIModel": {
"type": "string",
"enum": [
"Gemini"
],
"x-enum-varnames": [
"AI_MODEL_GEMINI"
]
},
"models.AlarmCode": { "models.AlarmCode": {
"type": "string", "type": "string",
"enum": [ "enum": [

View File

@@ -3363,6 +3363,59 @@
} }
} }
}, },
"/api/v1/feed/recipes/{id}/ai-diagnose": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "使用AI对指定配方进行点评并针对目标猪类型给出建议。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "AI点评配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "猪类型ID",
"name": "pig_type_id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表AI点评成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ReviewRecipeResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/adjust": { "/api/v1/inventory/stock/adjust": {
"post": { "post": {
"security": [ "security": [
@@ -9157,6 +9210,23 @@
} }
} }
}, },
"dto.ReviewRecipeResponse": {
"type": "object",
"properties": {
"ai_model": {
"description": "使用的 AI 模型",
"allOf": [
{
"$ref": "#/definitions/models.AIModel"
}
]
},
"review_message": {
"description": "点评内容",
"type": "string"
}
}
},
"dto.SellPigsRequest": { "dto.SellPigsRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -10076,6 +10146,15 @@
} }
} }
}, },
"models.AIModel": {
"type": "string",
"enum": [
"Gemini"
],
"x-enum-varnames": [
"AI_MODEL_GEMINI"
]
},
"models.AlarmCode": { "models.AlarmCode": {
"type": "string", "type": "string",
"enum": [ "enum": [

View File

@@ -1648,6 +1648,16 @@ definitions:
- quantity - quantity
- treatment_location - treatment_location
type: object type: object
dto.ReviewRecipeResponse:
properties:
ai_model:
allOf:
- $ref: '#/definitions/models.AIModel'
description: 使用的 AI 模型
review_message:
description: 点评内容
type: string
type: object
dto.SellPigsRequest: dto.SellPigsRequest:
properties: properties:
pen_id: pen_id:
@@ -2274,6 +2284,12 @@ definitions:
weight: weight:
type: number type: number
type: object type: object
models.AIModel:
enum:
- Gemini
type: string
x-enum-varnames:
- AI_MODEL_GEMINI
models.AlarmCode: models.AlarmCode:
enum: enum:
- 温度阈值 - 温度阈值
@@ -4755,6 +4771,37 @@ paths:
summary: 更新配方 summary: 更新配方
tags: tags:
- 饲料管理-配方 - 饲料管理-配方
/api/v1/feed/recipes/{id}/ai-diagnose:
get:
description: 使用AI对指定配方进行点评并针对目标猪类型给出建议。
parameters:
- description: 配方ID
in: path
name: id
required: true
type: integer
- description: 猪类型ID
in: query
name: pig_type_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 业务码为200代表AI点评成功
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ReviewRecipeResponse'
type: object
security:
- BearerAuth: []
summary: AI点评配方
tags:
- 饲料管理-配方
/api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}: /api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}:
post: post:
description: 根据指定的猪类型ID使用系统中所有可用的原料自动计算并创建一个成本最优的配方。 description: 根据指定的猪类型ID使用系统中所有可用的原料自动计算并创建一个成本最优的配方。

34
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/go-openapi/swag v0.25.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/generative-ai-go v0.20.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/labstack/echo/v4 v4.13.4 github.com/labstack/echo/v4 v4.13.4
github.com/panjf2000/ants/v2 v2.11.3 github.com/panjf2000/ants/v2 v2.11.3
@@ -20,7 +21,8 @@ require (
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.43.0
gonum.org/v1/gonum v0.16.0 gonum.org/v1/gonum v0.16.0
google.golang.org/protobuf v1.36.9 google.golang.org/api v0.256.0
google.golang.org/protobuf v1.36.10
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
gorm.io/datatypes v1.2.6 gorm.io/datatypes v1.2.6
@@ -29,11 +31,18 @@ require (
) )
require ( require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/longrunning v0.5.7 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.3 // 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.1 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect
@@ -52,7 +61,9 @@ require (
github.com/go-openapi/swag/typeutils v0.25.1 // indirect github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
@@ -72,18 +83,25 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.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 go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/grpc v1.76.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
gorm.io/driver/sqlite v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect

85
go.sum
View File

@@ -1,17 +1,38 @@
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
@@ -67,10 +88,20 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -107,6 +138,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -138,14 +171,22 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
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=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
@@ -160,21 +201,33 @@ 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/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 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/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 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/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -260,7 +260,8 @@ func (a *API) setupRoutes() {
feedGroup.GET("/recipes/:id", a.recipeController.GetRecipe) feedGroup.GET("/recipes/:id", a.recipeController.GetRecipe)
feedGroup.GET("/recipes", a.recipeController.ListRecipes) feedGroup.GET("/recipes", a.recipeController.ListRecipes)
feedGroup.POST("/recipes/generate-from-all-materials/:pig_type_id", a.recipeController.GenerateFromAllMaterials) feedGroup.POST("/recipes/generate-from-all-materials/:pig_type_id", a.recipeController.GenerateFromAllMaterials)
feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateFromAllMaterials) feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateRecipeWithPrioritizedStockRawMaterials)
feedGroup.GET("/recipes/:id/ai-diagnose", a.recipeController.AIDiagnoseRecipe)
} }
logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)")

View File

@@ -256,3 +256,48 @@ func (c *RecipeController) GenerateRecipeWithPrioritizedStockRawMaterials(ctx ec
logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID) logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp) return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp)
} }
// AIDiagnoseRecipe godoc
// @Summary AI点评配方
// @Description 使用AI对指定配方进行点评并针对目标猪类型给出建议。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param id path int true "配方ID"
// @Param pig_type_id query int true "猪类型ID"
// @Success 200 {object} controller.Response{data=dto.ReviewRecipeResponse} "业务码为200代表AI点评成功"
// @Router /api/v1/feed/recipes/{id}/ai-diagnose [get]
func (c *RecipeController) AIDiagnoseRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "AIDiagnoseRecipe")
const actionType = "AI点评配方"
// 从路径参数中获取配方ID
recipeIDStr := ctx.Param("id")
recipeID, err := strconv.ParseUint(recipeIDStr, 10, 32)
if err != nil {
logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, recipeIDStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", recipeIDStr)
}
// 从查询参数中获取猪类型ID
pigTypeIDStr := ctx.QueryParam("pig_type_id")
pigTypeID, err := strconv.ParseUint(pigTypeIDStr, 10, 32)
if err != nil {
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, pigTypeIDStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", pigTypeIDStr)
}
// 调用应用服务进行AI点评
reviewResponse, err := c.recipeService.AIDiagnoseRecipe(reqCtx, uint32(recipeID), uint32(pigTypeID))
if err != nil {
logger.Errorf("%s: 服务层AI点评失败: %v, RecipeID: %d, PigTypeID: %d", actionType, err, recipeID, pigTypeID)
if errors.Is(err, service.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方或猪类型不存在", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)})
}
// 对于其他错误,统一返回内部服务器错误
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "AI点评失败: "+err.Error(), actionType, "服务层AI点评失败", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)})
}
logger.Infof("%s: AI点评成功, RecipeID: %d, PigTypeID: %d", actionType, recipeID, pigTypeID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "AI点评成功", reviewResponse, actionType, "AI点评成功", reviewResponse)
}

View File

@@ -1,5 +1,7 @@
package dto package dto
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
// ============================================================================================================= // =============================================================================================================
// 营养种类 (Nutrient) 相关 DTO // 营养种类 (Nutrient) 相关 DTO
// ============================================================================================================= // =============================================================================================================
@@ -335,3 +337,14 @@ type GenerateRecipeResponse struct {
Name string `json:"name"` // 新生成的配方名称 Name string `json:"name"` // 新生成的配方名称
Description string `json:"description"` // 新生成的配方描述 Description string `json:"description"` // 新生成的配方描述
} }
// ReviewRecipeRequest 定义了点评配方的请求体
type ReviewRecipeRequest struct {
PigTypeID uint32 `json:"pig_type_id" binding:"required"` // 猪类型ID
}
// ReviewRecipeResponse 定义了点评配方的响应体
type ReviewRecipeResponse struct {
ReviewMessage string `json:"review_message"` // 点评内容
AIModel models.AIModel `json:"ai_model"` // 使用的 AI 模型
}

View File

@@ -29,6 +29,8 @@ type RecipeService interface {
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
// GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料
GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
// AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果和使用的AI模型
AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, error)
} }
// recipeServiceImpl 是 RecipeService 接口的实现 // recipeServiceImpl 是 RecipeService 接口的实现
@@ -175,3 +177,18 @@ func (s *recipeServiceImpl) ListRecipes(ctx context.Context, req *dto.ListRecipe
return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil
} }
// AIDiagnoseRecipe 实现智能诊断配方的方法
func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AIDiagnoseRecipe")
reviewMessage, aiModel, err := s.recipeSvc.AIDiagnoseRecipe(serviceCtx, recipeID, pigTypeID)
if err != nil {
return nil, fmt.Errorf("AI 诊断配方失败: %w", err)
}
return &dto.ReviewRecipeResponse{
ReviewMessage: reviewMessage,
AIModel: aiModel,
}, nil
}

View File

@@ -15,6 +15,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe" "git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
infra_ai "git.huangwc.com/pig/pig-farm-controller/internal/infra/ai"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database" "git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -34,6 +35,7 @@ type Infrastructure struct {
storage database.Storage storage database.Storage
repos *Repositories repos *Repositories
lora *LoraComponents lora *LoraComponents
ai infra_ai.AI
tokenGenerator token.Generator tokenGenerator token.Generator
} }
@@ -53,10 +55,17 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret)) tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
// 初始化 AI
ai, err := initAI(ctx, cfg.AI)
if err != nil {
return nil, fmt.Errorf("初始化 AI 管理器失败: %w", err)
}
return &Infrastructure{ return &Infrastructure{
storage: storage, storage: storage,
repos: repos, repos: repos,
lora: lora, lora: lora,
ai: ai,
tokenGenerator: tokenGenerator, tokenGenerator: tokenGenerator,
}, nil }, nil
} }
@@ -238,6 +247,8 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
pigTypeService, pigTypeService,
recipeCoreService, recipeCoreService,
recipeGenerateManager, recipeGenerateManager,
infra.repos.recipeRepo,
infra.ai,
) )
return &DomainServices{ return &DomainServices{
@@ -508,3 +519,12 @@ func initStorage(ctx context.Context, cfg config.DatabaseConfig) (database.Stora
logs.GetLogger(ctx).Info("数据库初始化完成。") logs.GetLogger(ctx).Info("数据库初始化完成。")
return storage, nil return storage, nil
} }
func initAI(ctx context.Context, cfg config.AIConfig) (infra_ai.AI, error) {
switch cfg.Model {
case models.AI_MODEL_GEMINI:
return infra_ai.NewGeminiAI(ctx, cfg.Gemini)
default:
return infra_ai.NewNoneAI(ctx), nil
}
}

View File

@@ -223,7 +223,7 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, areaControllerID uin
logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err) logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
return err return err
} }
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID) logger.Debugf("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID)
// 5. 构建最终的空中载荷 // 5. 构建最终的空中载荷
batchCmd := &proto.BatchCollectCommand{ batchCmd := &proto.BatchCollectCommand{
@@ -240,12 +240,12 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, areaControllerID uin
logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err) logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err)
return err return err
} }
logger.Infof("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction) logger.Debugf("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction)
if _, err := g.comm.Send(serviceCtx, networkID, payload); err != nil { if _, err := g.comm.Send(serviceCtx, networkID, payload); err != nil {
logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致", correlationID, err) logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致", correlationID, err)
return err return err
} }
logger.Infof("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID) logger.Debugf("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID)
return nil return nil
} }

View File

@@ -2,8 +2,11 @@ package recipe
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/ai"
"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"
@@ -22,6 +25,8 @@ type Service interface {
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
// GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料
GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
// AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果
AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error)
} }
// recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 // recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现
@@ -34,6 +39,9 @@ type recipeServiceImpl struct {
PigTypeService PigTypeService
RecipeCoreService RecipeCoreService
RecipeGenerateManager RecipeGenerateManager
recipeRepo repository.RecipeRepository
ai ai.AI
} }
// NewRecipeService 创建一个新的 Service 实例 // NewRecipeService 创建一个新的 Service 实例
@@ -46,6 +54,8 @@ func NewRecipeService(
pigTypeService PigTypeService, pigTypeService PigTypeService,
recipeCoreService RecipeCoreService, recipeCoreService RecipeCoreService,
recipeGenerateManager RecipeGenerateManager, recipeGenerateManager RecipeGenerateManager,
recipeRepo repository.RecipeRepository,
ai ai.AI,
) Service { ) Service {
return &recipeServiceImpl{ return &recipeServiceImpl{
ctx: ctx, ctx: ctx,
@@ -56,6 +66,8 @@ func NewRecipeService(
PigTypeService: pigTypeService, PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService, RecipeCoreService: recipeCoreService,
RecipeGenerateManager: recipeGenerateManager, RecipeGenerateManager: recipeGenerateManager,
recipeRepo: recipeRepo,
ai: ai,
} }
} }
@@ -86,6 +98,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex
} }
// 4. 丰富配方描述:计算并添加参考价格信息 // 4. 丰富配方描述:计算并添加参考价格信息
recipe.Name = fmt.Sprintf("%s - 使用所有已知原料", recipe.Name)
// 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本 // 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本
rawMaterialMap := make(map[uint32]models.RawMaterial) rawMaterialMap := make(map[uint32]models.RawMaterial)
@@ -120,7 +133,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex
if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil { if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil {
return nil, fmt.Errorf("保存生成的配方失败: %w", err) return nil, fmt.Errorf("保存生成的配方失败: %w", err)
} }
logger.Infof("成功生成配方: %+v", recipe) logger.Infof("成功生成配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description)
// 6. 返回创建的配方 (现在它应该已经有了ID) // 6. 返回创建的配方 (现在它应该已经有了ID)
return recipe, nil return recipe, nil
@@ -168,7 +181,8 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c
for _, mat := range stockMaterials { for _, mat := range stockMaterials {
adjustedMat := mat // 复制一份 adjustedMat := mat // 复制一份
// 大幅调低有库存原料的参考价格,诱导生成器优先使用 // 大幅调低有库存原料的参考价格,诱导生成器优先使用
adjustedMat.ReferencePrice = 0.01 // 设置一个非常小的价格 // TODO 按理说应该尽量优先使用已有原料, 但如果搭配后购买缺失原料花的钱还不如不用已有原料的另一个组合钱少怎么办
adjustedMat.ReferencePrice = adjustedMat.ReferencePrice * 0.1
materialsForGeneration = append(materialsForGeneration, adjustedMat) materialsForGeneration = append(materialsForGeneration, adjustedMat)
logger.Debugf("原料 '%s' (ID: %d) 有库存,生成配方时参考价格调整为 %.2f", mat.Name, mat.ID, adjustedMat.ReferencePrice) logger.Debugf("原料 '%s' (ID: %d) 有库存,生成配方时参考价格调整为 %.2f", mat.Name, mat.ID, adjustedMat.ReferencePrice)
} }
@@ -184,6 +198,8 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c
} }
// 5. 丰富配方描述:计算并添加参考价格信息 // 5. 丰富配方描述:计算并添加参考价格信息
recipe.Name = fmt.Sprintf("%s - 优先使用库存已有原料", recipe.Name)
// 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格 // 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格
// rawMaterialMap 从 allOriginalMaterials 构建,确保使用原始价格 // rawMaterialMap 从 allOriginalMaterials 构建,确保使用原始价格
rawMaterialMap := make(map[uint32]models.RawMaterial) rawMaterialMap := make(map[uint32]models.RawMaterial)
@@ -201,7 +217,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c
} }
referencePrice := recipe.CalculateReferencePricePerKilogram() / 100 referencePrice := recipe.CalculateReferencePricePerKilogram() / 100
recipe.Description = fmt.Sprintf("%s 计算时预估成本: %.2f元/kg。", recipe.Description, referencePrice) recipe.Description = fmt.Sprintf("使用 %v 种有库存原料和 %v 种无库存原料计算的库存原料优先使用的配方。 计算时预估成本: %.2f元/kg。", len(stockMaterials), len(noStockMaterials), referencePrice)
// 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。 // 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。
// 此时需要在描述中说明需要添加的廉价填充料的百分比。 // 此时需要在描述中说明需要添加的廉价填充料的百分比。
@@ -216,8 +232,118 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c
return nil, fmt.Errorf("保存生成的配方失败: %w", err) return nil, fmt.Errorf("保存生成的配方失败: %w", err)
} }
logger.Infof("成功生成优先使用库存原料的配方: %+v", recipe) logger.Infof("成功生成优先使用库存原料的配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description)
// 7. 返回创建的配方 // 7. 返回创建的配方
return recipe, nil return recipe, nil
} }
// AIDiagnoseRecipe 使用 AI 为指定食谱生成诊断。
func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error) {
serviceCtx, logger := logs.Trace(ctx, context.Background(), "AIDiagnoseRecipe")
// 1. 根据 recipeID 获取配方详情
recipe, err := s.recipeRepo.GetRecipeByID(serviceCtx, recipeID)
if err != nil {
logger.Errorf("获取配方详情失败: %v", err)
return "", s.ai.AIModel(), fmt.Errorf("获取配方详情失败: %w", err)
}
if recipe == nil {
logger.Warnf("未找到配方ID: %d", recipeID)
return "", s.ai.AIModel(), fmt.Errorf("未找到配方ID: %d", recipeID)
}
// 2. 获取目标猪只类型信息
pigType, err := s.GetPigTypeByID(serviceCtx, pigTypeID)
if err != nil {
logger.Errorf("获取猪只类型信息失败: %v", err)
return "", s.ai.AIModel(), fmt.Errorf("获取猪只类型信息失败: %w", err)
}
if pigType == nil {
logger.Warnf("未找到猪只类型ID: %d", pigTypeID)
return "", s.ai.AIModel(), fmt.Errorf("未找到猪只类型ID: %d", pigTypeID)
}
// 3. 定义 AI 输入结构体
type ingredientNutrient struct {
NutrientName string `json:"nutrient_name"`
Value float32 `json:"value"`
}
type recipeIngredient struct {
RawMaterialName string `json:"raw_material_name"`
Percentage float32 `json:"percentage"`
Nutrients []ingredientNutrient `json:"nutrients"`
}
type aiDiagnosisInput struct {
RecipeName string `json:"recipe_name"`
TargetPigType struct {
Name string `json:"name"`
} `json:"target_pig_type"`
Ingredients []recipeIngredient `json:"ingredients"`
}
// 4. 填充 AI 输入结构体
input := aiDiagnosisInput{
RecipeName: recipe.Name,
}
input.TargetPigType.Name = fmt.Sprintf("%s-%s", pigType.Breed.Name, pigType.AgeStage.Name)
for _, ingredient := range recipe.RecipeIngredients {
if ingredient.RawMaterial.ID == 0 {
logger.Warnf("配方成分中存在未加载的原料信息RecipeIngredientID: %d", ingredient.ID)
continue
}
ing := recipeIngredient{
RawMaterialName: ingredient.RawMaterial.Name,
Percentage: ingredient.Percentage,
}
for _, rmn := range ingredient.RawMaterial.RawMaterialNutrients {
if rmn.Nutrient.ID == 0 {
logger.Warnf("原料营养成分中存在未加载的营养素信息RawMaterialNutrientID: %d", rmn.ID)
continue
}
ing.Nutrients = append(ing.Nutrients, ingredientNutrient{
NutrientName: rmn.Nutrient.Name,
Value: rmn.Value,
})
}
input.Ingredients = append(input.Ingredients, ing)
}
// 5. 序列化为 JSON 字符串
jsonBytes, err := json.Marshal(input)
if err != nil {
logger.Errorf("序列化配方和猪只类型信息为 JSON 失败: %v", err)
return "", s.ai.AIModel(), fmt.Errorf("序列化数据失败: %w", err)
}
jsonString := string(jsonBytes)
// 6. 构建 AI Prompt
var promptBuilder strings.Builder
promptBuilder.WriteString(`
你是一个专业的动物营养师。请根据以下猪饲料配方数据,生成一份详细的、对养殖户友好的说明报告。
说明报告应包括以下部分:
1. 诊断猪只配方是否合理,如合理需要说明为什么合理, 如不合理需给出详细的改进建议。
2. 关键成分分析:分析主要原料和营养成分的作用
3. 使用建议:提供使用此配方的最佳实践和注意事项。
\n`)
promptBuilder.WriteString("```")
promptBuilder.WriteString(jsonString)
promptBuilder.WriteString("```")
prompt := promptBuilder.String()
logger.Debugf("生成的 AI 诊断 Prompt: \n%s", prompt)
// 7. 调用 AI Manager 进行诊断
diagnosisResult, err := s.ai.GenerateReview(serviceCtx, prompt)
if err != nil {
logger.Errorf("调用 AI Manager 诊断配方失败: %v", err)
return "", s.ai.AIModel(), fmt.Errorf("AI 诊断失败: %w", err)
}
logger.Infof("成功对配方 ID: %d (目标猪只类型 ID: %d) 进行 AI 诊断。", recipeID, pigTypeID)
return diagnosisResult, s.ai.AIModel(), nil
}

19
internal/infra/ai/ai.go Normal file
View File

@@ -0,0 +1,19 @@
package ai
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// AI 定义了通用的 AI 管理接口。
// 它可以用于处理各种 AI 相关的任务,例如文本生成、内容审核等。
type AI interface {
// GenerateReview 根据提供的文本内容生成评论。
// prompt: 用于生成评论的输入文本。
// 返回生成的评论字符串和可能发生的错误。
GenerateReview(ctx context.Context, prompt string) (string, error)
// AIModel 返回当前使用的 AI 模型。
AIModel() models.AIModel
}

View File

@@ -0,0 +1,73 @@
package ai
import (
"context"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
// geminiImpl 是 Gemini AI 服务的实现。
type geminiImpl struct {
client *genai.GenerativeModel
cfg config.Gemini
}
// NewGeminiAI 创建一个新的 geminiImpl 实例。
func NewGeminiAI(ctx context.Context, cfg config.Gemini) (AI, error) {
// 检查 API Key 是否存在
if cfg.APIKey == "" {
return nil, fmt.Errorf("Gemini API Key 未配置")
}
// 创建 Gemini 客户端
genaiClient, err := genai.NewClient(ctx, option.WithAPIKey(cfg.APIKey))
if err != nil {
return nil, fmt.Errorf("创建 Gemini 客户端失败: %w", err)
}
return &geminiImpl{
client: genaiClient.GenerativeModel(cfg.ModelName),
cfg: cfg,
}, nil
}
// GenerateReview 根据提供的文本内容生成评论。
func (g *geminiImpl) GenerateReview(ctx context.Context, prompt string) (string, error) {
serviceCtx, logger := logs.Trace(ctx, context.Background(), "GenerateReview")
logger.Debugf("开始调用 Gemini 生成评论prompt: %s", prompt)
timeoutCtx, cancel := context.WithTimeout(serviceCtx, time.Duration(g.cfg.Timeout)*time.Second)
defer cancel()
resp, err := g.client.GenerateContent(timeoutCtx, genai.Text(prompt))
if err != nil {
logger.Errorf("调用 Gemini API 失败: %v", err)
return "", fmt.Errorf("调用 Gemini API 失败: %w", err)
}
if resp == nil || len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
logger.Warn("Gemini API 返回空内容或无候选评论")
return "", fmt.Errorf("Gemini API 返回空内容或无候选评论")
}
var review string
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
review += string(txt)
}
}
logger.Debugf("成功从 Gemini 生成评论: %s", review)
return review, nil
}
func (g *geminiImpl) AIModel() models.AIModel {
return models.AI_MODEL_GEMINI
}

View File

@@ -0,0 +1,31 @@
package ai
import (
"context"
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
var NoneAIError = errors.New("当前没有配置AI, 暂不支持此功能")
type NoneAI struct {
ctx context.Context
}
func NewNoneAI(ctx context.Context) AI {
return &NoneAI{
ctx: ctx,
}
}
func (n *NoneAI) GenerateReview(ctx context.Context, prompt string) (string, error) {
logger := logs.TraceLogger(ctx, n.ctx, "GenerateReview")
logger.Warnf("当前没有配置AI, 无法处理AI请求, 消息: %s", prompt)
return "", NoneAIError
}
func (n *NoneAI) AIModel() models.AIModel {
return models.AI_MODEL_NONE
}

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@@ -50,6 +51,9 @@ type Config struct {
// AlarmNotification 告警通知配置 // AlarmNotification 告警通知配置
AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"` AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"`
// AI AI服务配置
AI AIConfig `yaml:"ai"`
} }
// AppConfig 代表应用基础配置 // AppConfig 代表应用基础配置
@@ -231,6 +235,19 @@ type AlarmNotificationConfig struct {
NotificationIntervals NotificationIntervalsConfig `yaml:"notification_intervals"` NotificationIntervals NotificationIntervalsConfig `yaml:"notification_intervals"`
} }
// AIConfig AI 服务配置
type AIConfig struct {
Model models.AIModel `yaml:"model"`
Gemini Gemini `yaml:"gemini"`
}
// Gemini 代表 Gemini AI 服务的配置
type Gemini struct {
APIKey string `yaml:"api_key"` // Gemini API Key
ModelName string `yaml:"model_name"` // Gemini 模型名称,例如 "gemini-pro"
Timeout int `yaml:"timeout"` // AI 请求超时时间 (秒)
}
// NewConfig 创建并返回一个新的配置实例 // NewConfig 创建并返回一个新的配置实例
func NewConfig() *Config { func NewConfig() *Config {
// 默认值可以在这里设置,但我们优先使用配置文件中的值 // 默认值可以在这里设置,但我们优先使用配置文件中的值

View File

@@ -13,6 +13,7 @@ import (
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
@@ -65,8 +66,8 @@ func NewLogger(cfg config.LogConfig) *Logger {
// 5. 构建 Logger // 5. 构建 Logger
// zap.AddCaller() 会记录调用日志的代码行 // zap.AddCaller() 会记录调用日志的代码行
// zap.AddCallerSkip(1) 可以向上跳层调用栈,如果我们将 logger.Info 等方法再封装一层,这个选项会很有用 // zap.AddCallerSkip(2) 可以向上跳层调用栈,因为我们的日志方法被封装了两层 (Logger.Info -> Logger.logWithTrace)
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2))
return &Logger{sl: zapLogger.Sugar()} return &Logger{sl: zapLogger.Sugar()}
} }

View File

@@ -12,6 +12,13 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type AIModel string
const (
AI_MODEL_NONE AIModel = "None"
AI_MODEL_GEMINI AIModel = "Gemini"
)
// Model 用于代替gorm.Model, 使用uint32以节约空间 // Model 用于代替gorm.Model, 使用uint32以节约空间
type Model struct { type Model struct {
ID uint32 `gorm:"primarykey"` ID uint32 `gorm:"primarykey"`

View File

@@ -18,7 +18,7 @@ type RawMaterialListOptions struct {
NutrientName *string NutrientName *string
MinReferencePrice *float32 // 参考价格最小值 MinReferencePrice *float32 // 参考价格最小值
MaxReferencePrice *float32 // 参考价格最大值 MaxReferencePrice *float32 // 参考价格最大值
HasStock *bool // 是否只查询有库存的原料 HasStock *bool
OrderBy string OrderBy string
} }
@@ -123,20 +123,27 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R
db = db.Where("reference_price <= ?", *opts.MaxReferencePrice) db = db.Where("reference_price <= ?", *opts.MaxReferencePrice)
} }
// 筛选有库存的原料 // 筛选有/无库存的原料
if opts.HasStock != nil && *opts.HasStock { if opts.HasStock != nil {
// 内部子查询:生成带有 rn 的结果集GORM 会自动为 models.RawMaterialStockLog 添加 deleted_at IS NULL // 内部子查询:生成带有 rn 的结果集GORM 会自动为 models.RawMaterialStockLog 添加 deleted_at IS NULL
rankedLogsQuery := r.db.Model(&models.RawMaterialStockLog{}). rankedLogsQuery := r.db.Model(&models.RawMaterialStockLog{}).
Select("raw_material_id, after_quantity, ROW_NUMBER() OVER(PARTITION BY raw_material_id ORDER BY happened_at DESC, id DESC) as rn") Select("raw_material_id, after_quantity, ROW_NUMBER() OVER(PARTITION BY raw_material_id ORDER BY happened_at DESC, id DESC) as rn")
// 外部子查询:从 ranked_logs 中筛选 rn=1 且 after_quantity > 0 的 raw_material_id // 外部子查询:从 ranked_logs 中筛选 rn=1 的 raw_material_id
// GORM 会将 rankedLogsQuery 作为一个子查询嵌入到 FROM 子句中
latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery). latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery).
Select("raw_material_id"). Select("raw_material_id").
Where("rn = 1 AND after_quantity > 0") Where("rn = 1").
Where("after_quantity > 0")
// 将这个子查询直接应用到主查询的 WHERE id IN (?) 条件中 if *opts.HasStock {
// 筛选有库存的原料 (ID 在有正库存的集合中)
db = db.Where("id IN (?)", latestStockLogSubQuery) db = db.Where("id IN (?)", latestStockLogSubQuery)
} else {
// 筛选无库存的原料 (ID 不在有正库存的集合中)
// 包含了最新库存为0 和 没有库存日志的原料。
db = db.Where("id NOT IN (?)", latestStockLogSubQuery)
}
} }
// 首先计算总数 // 首先计算总数

View File

@@ -266,7 +266,7 @@ func (t *LoRaMeshUartPassthroughTransport) executeSend(ctx context.Context, req
frame.WriteByte(currentChunk) // 当前包序号 frame.WriteByte(currentChunk) // 当前包序号
frame.Write(chunk) // 数据块 frame.Write(chunk) // 数据块
logger.Infof("构建LoRa数据包: %v", frame.Bytes()) logger.Debugf("构建LoRa数据包: %v", frame.Bytes())
_, err := t.port.Write(frame.Bytes()) _, err := t.port.Write(frame.Bytes())
if err != nil { if err != nil {
return nil, fmt.Errorf("写入串口失败: %w", err) return nil, fmt.Errorf("写入串口失败: %w", err)

View File

@@ -37,7 +37,7 @@ design/archive/2025-11-05-provide-logger-with-mothed/task-webhook.md
design/archive/2025-11-06-health-check-routing/index.md design/archive/2025-11-06-health-check-routing/index.md
design/archive/2025-11-06-system-plan-continuously-triggered/index.md design/archive/2025-11-06-system-plan-continuously-triggered/index.md
design/archive/2025-11-10-exceeding-threshold-alarm/index.md design/archive/2025-11-10-exceeding-threshold-alarm/index.md
design/recipe-management/index.md design/archive/2025-11-29-recipe-management/index.md
docs/docs.go docs/docs.go
docs/swagger.json docs/swagger.json
docs/swagger.yaml docs/swagger.yaml