Compare commits
10 Commits
4ffc41ea84
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 37b9055994 | |||
| c5aa1c6f2c | |||
| 3aae619af5 | |||
| e9c93d4029 | |||
| b2e6e511cd | |||
| ae0020aa71 | |||
| b17d233cd5 | |||
| 3f05dccdce | |||
| 9d4a5c6375 | |||
| 3f50901ac6 |
@@ -0,0 +1,188 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/utils/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AiChatApi struct{}
|
||||
|
||||
// ChatStreamPlant 接收用户输入,基于植物百科 RAG 产生流式 SSE 回答
|
||||
// @Summary 植物助手聊天(SSE 流式输出)
|
||||
// @Tags Plant-AiChat
|
||||
// @Param query query string true "用户提问内容"
|
||||
// @Produce text/event-stream
|
||||
// @Router /plant/chat/stream [get]
|
||||
func (a *AiChatApi) ChatStreamPlant(c *gin.Context) {
|
||||
query := c.Query("query")
|
||||
if query == "" {
|
||||
response.FailWithMsg("参数 query 不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
userId := auth.GetUserId(c)
|
||||
|
||||
// ── 每日用量检查 ──
|
||||
cfg, cfgErr := sysAiConfigService.GetActiveAiConfig()
|
||||
if cfgErr == nil && cfg.DailyQueryLimit > 0 {
|
||||
todayCount, _ := chatHistoryService.GetTodayCount(userId)
|
||||
if todayCount >= int64(cfg.DailyQueryLimit) {
|
||||
response.FailWithMsg(fmt.Sprintf("今日问答次数已达上限(%d次),明天再来吧", cfg.DailyQueryLimit), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SSE Headers
|
||||
w := c.Writer
|
||||
header := w.Header()
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Transfer-Encoding", "chunked")
|
||||
w.WriteHeader(200)
|
||||
|
||||
var fullAnswer string
|
||||
err := aiRagService.PlantChatStreamRAG(c.Request.Context(), query, func(chunk string) error {
|
||||
fullAnswer += chunk
|
||||
_, writeErr := w.WriteString("data: " + chunk + "\n\n")
|
||||
w.Flush()
|
||||
return writeErr
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
global.Logger.Error("PlantChatStreamRAG error", zap.Error(err))
|
||||
_, _ = w.WriteString("data: [ERROR]" + err.Error() + "\n\n")
|
||||
w.Flush()
|
||||
} else {
|
||||
_, _ = w.WriteString("data: [DONE]\n\n")
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
// 异步保存问答历史
|
||||
if fullAnswer != "" {
|
||||
go func() {
|
||||
if saveErr := chatHistoryService.SaveHistory(userId, query, fullAnswer); saveErr != nil {
|
||||
global.Logger.Error("Save chat history failed", zap.Error(saveErr))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// SyncWikiToQdrant 手动触发全量植物百科同步到 Qdrant
|
||||
// @Summary 同步植物百科数据到 Qdrant 向量库
|
||||
// @Tags System-SysAiConfig
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /plant/chat/sync [post]
|
||||
func (a *AiChatApi) SyncWikiToQdrant(c *gin.Context) {
|
||||
if err := aiRagService.SyncWikiToQdrant(); err != nil {
|
||||
global.Logger.Error("SyncWikiToQdrant error", zap.Error(err))
|
||||
response.FailWithMsg("同步失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("同步成功", c)
|
||||
}
|
||||
|
||||
// GetChatHistory 获取AI问答历史列表
|
||||
// @Tags Plant-AiChat
|
||||
// @Param current query int false "页码"
|
||||
// @Param pageSize query int false "每页条数"
|
||||
// @Router /plant/chat/history [get]
|
||||
func (a *AiChatApi) GetChatHistory(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
current := 1
|
||||
pageSize := 20
|
||||
if v, ok := c.GetQuery("current"); ok {
|
||||
if n := parseInt(v); n > 0 {
|
||||
current = n
|
||||
}
|
||||
}
|
||||
if v, ok := c.GetQuery("pageSize"); ok {
|
||||
if n := parseInt(v); n > 0 {
|
||||
pageSize = n
|
||||
}
|
||||
}
|
||||
list, total, err := chatHistoryService.GetHistoryList(userId, current, pageSize)
|
||||
if err != nil {
|
||||
response.FailWithMsg("查询失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// DeleteChatHistory 删除单条历史
|
||||
// @Tags Plant-AiChat
|
||||
// @Router /plant/chat/history/delete [post]
|
||||
func (a *AiChatApi) DeleteChatHistory(c *gin.Context) {
|
||||
var req struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Id == "" {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
userId := auth.GetUserId(c)
|
||||
if err := chatHistoryService.DeleteHistory(userId, req.Id); err != nil {
|
||||
response.FailWithMsg("删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
|
||||
// ClearChatHistory 清空所有历史
|
||||
// @Tags Plant-AiChat
|
||||
// @Router /plant/chat/history/clear [post]
|
||||
func (a *AiChatApi) ClearChatHistory(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
if err := chatHistoryService.ClearHistory(userId); err != nil {
|
||||
response.FailWithMsg("清空失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("已清空", c)
|
||||
}
|
||||
|
||||
// GetChatQuota 获取当前用户今日剩余问答额度
|
||||
// @Tags Plant-AiChat
|
||||
// @Router /plant/chat/quota [get]
|
||||
func (a *AiChatApi) GetChatQuota(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
todayCount, _ := chatHistoryService.GetTodayCount(userId)
|
||||
|
||||
limit := 0 // 0 = unlimited
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err == nil && cfg.DailyQueryLimit > 0 {
|
||||
limit = cfg.DailyQueryLimit
|
||||
}
|
||||
|
||||
remaining := -1 // -1 means unlimited
|
||||
if limit > 0 {
|
||||
remaining = limit - int(todayCount)
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
}
|
||||
|
||||
response.OkWithData(map[string]interface{}{
|
||||
"used": todayCount,
|
||||
"limit": limit,
|
||||
"remaining": remaining,
|
||||
}, c)
|
||||
}
|
||||
|
||||
func parseInt(s string) int {
|
||||
n := 0
|
||||
for _, c := range s {
|
||||
if c >= '0' && c <= '9' {
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/plant"
|
||||
"sundynix-go/model/plant/request"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type BannerApi struct{}
|
||||
|
||||
// Create 创建Banner
|
||||
// @Tags 轮播图管理
|
||||
// @Summary 创建Banner
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body plant.Banner true "创建Banner"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
|
||||
// @Router /plantBanner/create [post]
|
||||
func (a *BannerApi) Create(c *gin.Context) {
|
||||
var req plant.Banner
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := bannerService.Create(req); err != nil {
|
||||
global.Logger.Error("创建失败", zap.Error(err))
|
||||
response.FailWithMsg("创建失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("创建成功", c)
|
||||
}
|
||||
|
||||
// Delete 删除Banner
|
||||
// @Tags 轮播图管理
|
||||
// @Summary 删除Banner
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body object true "删除Banner"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||
// @Router /plantBanner/delete [post]
|
||||
func (a *BannerApi) Delete(c *gin.Context) {
|
||||
var req struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Id == "" {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := bannerService.Delete(req.Id); err != nil {
|
||||
global.Logger.Error("删除失败", zap.Error(err))
|
||||
response.FailWithMsg("删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
|
||||
// Update 更新Banner
|
||||
// @Tags 轮播图管理
|
||||
// @Summary 更新Banner
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body plant.Banner true "更新Banner"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
|
||||
// @Router /plantBanner/update [put]
|
||||
func (a *BannerApi) Update(c *gin.Context) {
|
||||
var req plant.Banner
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := bannerService.Update(req); err != nil {
|
||||
global.Logger.Error("更新失败", zap.Error(err))
|
||||
response.FailWithMsg("更新失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// GetList 分页获取Banner
|
||||
// @Tags 轮播图管理
|
||||
// @Summary 分页获取Banner
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.BannerPageReq true "分页获取Banner"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
|
||||
// @Router /plantBanner/list [post]
|
||||
func (a *BannerApi) GetList(c *gin.Context) {
|
||||
var req request.BannerPageReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
list, total, err := bannerService.GetList(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Current,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// GetActiveList 客户端获取启用的Banner
|
||||
// @Tags 轮播图管理
|
||||
// @Summary 客户端获取启用的Banner
|
||||
// @Produce application/json
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
|
||||
// @Router /plantBanner/activeList [get]
|
||||
func (a *BannerApi) GetActiveList(c *gin.Context) {
|
||||
list, err := bannerService.GetActiveList()
|
||||
if err != nil {
|
||||
global.Logger.Error("获取失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.ListResult{
|
||||
List: list,
|
||||
}, c)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
plantRes "sundynix-go/model/plant/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CallbackApi struct{}
|
||||
|
||||
// MediaCheckCallback 微信内容安全回调接口
|
||||
func (a *CallbackApi) MediaCheckCallback(c *gin.Context) {
|
||||
var cb plantRes.WeChatCheckResultCallback
|
||||
if err := c.ShouldBindJSON(&cb); err != nil {
|
||||
global.Logger.Error("解析微信回调JSON失败", zap.Error(err))
|
||||
response.FailWithMsg("解析失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := callbackService.HandleMediaCheckCallback(cb); err != nil {
|
||||
global.Logger.Error("处理微信回调失败", zap.Error(err))
|
||||
response.FailWithMsg("处理失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("success", c)
|
||||
}
|
||||
@@ -13,6 +13,9 @@ type ApiGroup struct {
|
||||
UserProfileApi
|
||||
BadgeConfigApi
|
||||
CallbackApi
|
||||
ExchangeApi
|
||||
AiChatApi
|
||||
BannerApi
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -26,4 +29,9 @@ var (
|
||||
userProfileService = service.GroupApp.PlantServiceGroup.UserProfileService
|
||||
badgeConfigService = service.GroupApp.PlantServiceGroup.BadgeConfigService
|
||||
callbackService = service.GroupApp.PlantServiceGroup.CallbackService
|
||||
exchangeService = service.GroupApp.PlantServiceGroup.ExchangeService
|
||||
aiRagService = service.GroupApp.PlantServiceGroup.AiRagService
|
||||
chatHistoryService = service.GroupApp.PlantServiceGroup.AiChatHistoryService
|
||||
sysAiConfigService = service.GroupApp.SystemServiceGroup.SysAiConfigService
|
||||
bannerService = service.GroupApp.PlantServiceGroup.BannerService
|
||||
)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/plant/request"
|
||||
"sundynix-go/utils/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ExchangeApi struct{}
|
||||
|
||||
// ==================== 管理端 API ====================
|
||||
|
||||
// CreateItem 创建兑换商品
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 创建兑换商品
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeItemCreateReq true "创建兑换商品"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
|
||||
// @Router /exchange/item/create [post]
|
||||
func (a *ExchangeApi) CreateItem(c *gin.Context) {
|
||||
var req request.ExchangeItemCreateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := exchangeService.CreateItem(req); err != nil {
|
||||
global.Logger.Error("创建兑换商品失败", zap.Error(err))
|
||||
response.FailWithMsg("创建失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("创建成功", c)
|
||||
}
|
||||
|
||||
// UpdateItem 更新兑换商品
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 更新兑换商品
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeItemUpdateReq true "更新兑换商品"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
|
||||
// @Router /exchange/item/update [post]
|
||||
func (a *ExchangeApi) UpdateItem(c *gin.Context) {
|
||||
var req request.ExchangeItemUpdateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := exchangeService.UpdateItem(req); err != nil {
|
||||
global.Logger.Error("更新兑换商品失败", zap.Error(err))
|
||||
response.FailWithMsg("更新失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteItem 删除兑换商品
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 删除兑换商品
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeItemCreateReq true "删除兑换商品"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||
// @Router /exchange/item/delete [post]
|
||||
func (a *ExchangeApi) DeleteItem(c *gin.Context) {
|
||||
var req struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := exchangeService.DeleteItem(req.Id); err != nil {
|
||||
global.Logger.Error("删除兑换商品失败", zap.Error(err))
|
||||
response.FailWithMsg("删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
|
||||
// AdminItemList 管理端商品列表
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 管理端商品列表
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeItemListReq true "分页获取商品列表"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /exchange/item/list [post]
|
||||
func (a *ExchangeApi) AdminItemList(c *gin.Context) {
|
||||
var req request.ExchangeItemListReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
list, total, err := exchangeService.AdminItemList(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取商品列表失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Current,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// AdminOrderList 管理端订单列表
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 管理端订单列表
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeOrderListReq true "分页获取订单列表"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /exchange/order/list [post]
|
||||
func (a *ExchangeApi) AdminOrderList(c *gin.Context) {
|
||||
var req request.ExchangeOrderListReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
list, total, err := exchangeService.AdminOrderList(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取订单列表失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Current,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// UpdateOrderStatus 更新订单状态
|
||||
// @Tags 兑换中心-管理
|
||||
// @Summary 更新订单状态
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeOrderUpdateReq true "更新订单状态"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
|
||||
// @Router /exchange/order/update [post]
|
||||
func (a *ExchangeApi) UpdateOrderStatus(c *gin.Context) {
|
||||
var req request.ExchangeOrderUpdateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := exchangeService.UpdateOrderStatus(req); err != nil {
|
||||
global.Logger.Error("更新订单状态失败", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// ==================== 用户端 API ====================
|
||||
|
||||
// UserItemList 用户端商品列表
|
||||
// @Tags 兑换中心-用户
|
||||
// @Summary 用户端商品列表
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Param current query int false "页码"
|
||||
// @Param pageSize query int false "每页大小"
|
||||
// @Param type query string false "商品类型"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /exchange/list [get]
|
||||
func (a *ExchangeApi) UserItemList(c *gin.Context) {
|
||||
var req request.ExchangeItemListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
list, total, err := exchangeService.UserItemList(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取商品列表失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Current,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// UserItemDetail 商品详情
|
||||
// @Tags 兑换中心-用户
|
||||
// @Summary 商品详情
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "商品ID"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /exchange/detail [get]
|
||||
func (a *ExchangeApi) UserItemDetail(c *gin.Context) {
|
||||
itemId := c.Query("id")
|
||||
if itemId == "" {
|
||||
response.FailWithMsg("参数错误", c)
|
||||
return
|
||||
}
|
||||
item, err := exchangeService.UserItemDetail(itemId)
|
||||
if err != nil {
|
||||
response.FailWithMsg("商品不存在", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(item, c)
|
||||
}
|
||||
|
||||
// UserExchange 用户发起兑换
|
||||
// @Tags 兑换中心-用户
|
||||
// @Summary 发起兑换
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ExchangeReq true "兑换请求"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"兑换成功"}"
|
||||
// @Router /exchange/redeem [post]
|
||||
func (a *ExchangeApi) UserExchange(c *gin.Context) {
|
||||
var req request.ExchangeReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
userId := auth.GetUserId(c)
|
||||
if err := exchangeService.UserExchange(req, userId); err != nil {
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("兑换成功", c)
|
||||
}
|
||||
|
||||
// UserOrderList 用户订单列表
|
||||
// @Tags 兑换中心-用户
|
||||
// @Summary 用户兑换记录
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Param current query int false "页码"
|
||||
// @Param pageSize query int false "每页大小"
|
||||
// @Param status query int false "订单状态"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /exchange/orders [get]
|
||||
func (a *ExchangeApi) UserOrderList(c *gin.Context) {
|
||||
var req request.ExchangeOrderListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
userId := auth.GetUserId(c)
|
||||
list, total, err := exchangeService.UserOrderList(req, userId)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取订单列表失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Current,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
@@ -277,3 +277,28 @@ func (a *MyPlantApi) AddGrowthRecord(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMsg("添加成长记录成功", c)
|
||||
}
|
||||
|
||||
// QuickCare 快捷养护记录
|
||||
// @Tags 我的植物
|
||||
// @Summary 快捷养护记录(无需预设任务)
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body plantReq.QuickCare true "快捷养护"
|
||||
// @Router /plant/quickCare [post]
|
||||
func (a *MyPlantApi) QuickCare(c *gin.Context) {
|
||||
var req plantReq.QuickCare
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
userId := auth.GetUserId(c)
|
||||
err = plantService.QuickCare(req, userId)
|
||||
if err != nil {
|
||||
global.Logger.Error("快捷养护记录失败", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("养护记录已保存", c)
|
||||
}
|
||||
|
||||
@@ -82,3 +82,23 @@ func (a *UserProfileApi) MyStars(c *gin.Context) {
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// MyBadges 我的徽章
|
||||
// @Tags 个人中心
|
||||
// @Summary 我的徽章
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||
// @Router /profile/badge [get]
|
||||
func (a *UserProfileApi) MyBadges(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
list, err := userProfileService.MyBadges(userId)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取用户徽章失败", zap.Error(err))
|
||||
response.FailWithMsg("获取用户徽章失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.ListResult{
|
||||
List: list,
|
||||
}, c)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
"sundynix-go/model/commom/response"
|
||||
plantReq "sundynix-go/model/plant/request"
|
||||
"sundynix-go/utils/auth"
|
||||
@@ -113,6 +114,73 @@ func (a *WikiApi) WikiDetail(c *gin.Context) {
|
||||
response.OkWithData(topic, c)
|
||||
}
|
||||
|
||||
// DeleteWiki 删除百科
|
||||
// @Tags 百科
|
||||
// @Summary 删除百科
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body common.IdsReq true "删除百科"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||
// @Router /wiki/delete [post]
|
||||
func (a *WikiApi) DeleteWiki(c *gin.Context) {
|
||||
var req common.IdsReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
err = wikiService.DeleteWiki(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("删除失败", zap.Error(err))
|
||||
response.FailWithMsg("删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
|
||||
// SyncWikiQdrant 单条百科同步到 Qdrant
|
||||
// @Tags 百科
|
||||
// @Summary 百科同步到Qdrant
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Param data body common.GetById true "单条百科"
|
||||
// @Router /wiki/sync-qdrant [post]
|
||||
func (a *WikiApi) SyncWikiQdrant(c *gin.Context) {
|
||||
var req common.GetById
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := aiRagService.SyncSingleWiki(req.ID); err != nil {
|
||||
global.Logger.Error("同步 Qdrant 失败", zap.Error(err))
|
||||
response.FailWithMsg("同步失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("同步成功", c)
|
||||
}
|
||||
|
||||
// DeleteWikiQdrant 从 Qdrant 移除单条百科向量
|
||||
// @Tags 百科
|
||||
// @Summary 百科移除Qdrant
|
||||
// @Security BearerAuth
|
||||
// @Produce application/json
|
||||
// @Param data body common.GetById true "单条百科"
|
||||
// @Router /wiki/delete-qdrant [post]
|
||||
func (a *WikiApi) DeleteWikiQdrant(c *gin.Context) {
|
||||
var req common.GetById
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := aiRagService.DeleteFromQdrant(req.ID); err != nil {
|
||||
global.Logger.Error("从 Qdrant 删除向量失败", zap.Error(err))
|
||||
response.FailWithMsg("移除失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("移除成功", c)
|
||||
}
|
||||
|
||||
// StarWiki 收藏百科
|
||||
// @Tags 百科
|
||||
// @Summary 收藏百科
|
||||
@@ -134,3 +202,28 @@ func (a *WikiApi) StarWiki(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMsg("操作成功", c)
|
||||
}
|
||||
|
||||
// UploadImg 上传图片
|
||||
// @Tags 百科
|
||||
// @Summary 上传图片
|
||||
// @Security BearerAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body common.UploadOss true "上传图片"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"上传成功"}"
|
||||
// @Router /wiki/uploadImg [post]
|
||||
func (a *WikiApi) UploadImg(c *gin.Context) {
|
||||
var req common.UploadOss
|
||||
err := c.ShouldBind(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
err = wikiService.UploadImg(req)
|
||||
if err != nil {
|
||||
global.Logger.Error("上传失败", zap.Error(err))
|
||||
response.FailWithMsg("上传失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("上传成功", c)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type ApiGroup struct {
|
||||
MenuApi
|
||||
OperationRecordApi
|
||||
OssApi
|
||||
SysAiConfigApi
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -20,4 +21,5 @@ var (
|
||||
menuService = service.GroupApp.SystemServiceGroup.MenuService
|
||||
operationRecordService = service.GroupApp.SystemServiceGroup.OperationRecordService
|
||||
ossService = service.GroupApp.SystemServiceGroup.OssService
|
||||
sysAiConfigService = service.GroupApp.SystemServiceGroup.SysAiConfigService
|
||||
)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
type SysAiConfigApi struct{}
|
||||
|
||||
// CreateAiConfig 创建 AI 配置
|
||||
// @Summary 创建 AI 配置
|
||||
// @Tags System-SysAiConfig
|
||||
// @accept json
|
||||
// @Produce json
|
||||
// @Param data body system.SysAiConfig true "配置模型"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /aiConfig/create [post]
|
||||
func (a *SysAiConfigApi) CreateAiConfig(c *gin.Context) {
|
||||
var cfg system.SysAiConfig
|
||||
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||
response.FailWithMsg("参数错误:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if err := sysAiConfigService.Create(&cfg); err != nil {
|
||||
global.Logger.Error("创建AI配置失败", zap.Error(err))
|
||||
response.FailWithMsg("创建失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("创建成功", c)
|
||||
}
|
||||
|
||||
// UpdateAiConfig 更新 AI 配置
|
||||
// @Summary 更新 AI 配置
|
||||
// @Tags System-SysAiConfig
|
||||
// @accept json
|
||||
// @Produce json
|
||||
// @Param data body system.SysAiConfig true "配置模型"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /aiConfig/update [put]
|
||||
func (a *SysAiConfigApi) UpdateAiConfig(c *gin.Context) {
|
||||
var cfg system.SysAiConfig
|
||||
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||
response.FailWithMsg("参数错误:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if err := sysAiConfigService.Update(&cfg); err != nil {
|
||||
global.Logger.Error("更新AI配置失败", zap.Error(err))
|
||||
response.FailWithMsg("更新失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// SetActive 设置激活配置
|
||||
// @Summary 设置激活配置(同一时间只有一条激活)
|
||||
// @Tags System-SysAiConfig
|
||||
// @accept json
|
||||
// @Produce json
|
||||
// @Param data body object true "{ \"id\": \"xxx\" }"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /aiConfig/setActive [post]
|
||||
func (a *SysAiConfigApi) SetActive(c *gin.Context) {
|
||||
var body struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.FailWithMsg("参数错误:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if err := sysAiConfigService.SetActive(body.Id); err != nil {
|
||||
global.Logger.Error("设置激活AI配置失败", zap.Error(err))
|
||||
response.FailWithMsg("设置失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMsg("设置成功", c)
|
||||
}
|
||||
|
||||
// GetList 获取配置列表
|
||||
// @Summary 获取 AI 配置列表
|
||||
// @Tags System-SysAiConfig
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /aiConfig/list [get]
|
||||
func (a *SysAiConfigApi) GetList(c *gin.Context) {
|
||||
list, err := sysAiConfigService.GetList()
|
||||
if err != nil {
|
||||
global.Logger.Error("获取AI配置列表失败", zap.Error(err))
|
||||
response.FailWithMsg("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: int64(len(list)),
|
||||
}, c)
|
||||
}
|
||||
@@ -13,8 +13,10 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
github.com/mojocn/base64Captcha v1.3.8
|
||||
github.com/qdrant/go-client v1.17.1
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
@@ -22,8 +24,9 @@ require (
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.70
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.26.0
|
||||
@@ -75,7 +78,7 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
@@ -104,12 +107,13 @@ require (
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/image v0.26.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.64.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
@@ -47,6 +47,10 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
|
||||
@@ -96,6 +100,8 @@ github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJD
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -123,8 +129,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -163,6 +169,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g=
|
||||
github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -176,6 +184,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
@@ -218,6 +228,18 @@ github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -231,8 +253,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
@@ -243,8 +265,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -254,8 +276,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -263,8 +285,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -276,8 +298,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
@@ -295,19 +317,25 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
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/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+7
-1
@@ -38,13 +38,15 @@ func MigrateTable() {
|
||||
system.Menu{},
|
||||
system.SysOperationRecord{},
|
||||
system.Oss{},
|
||||
system.SysAiConfig{},
|
||||
|
||||
plant.Banner{}, //轮播图
|
||||
plant.MyPlant{}, //我的植物
|
||||
plant.CarePlan{}, //植物养护计划
|
||||
plant.CareTask{}, //植物养护任务
|
||||
plant.CareRecord{}, //植物养护记录
|
||||
plant.GrowthRecord{}, //植物成长记录
|
||||
plant.MediaCheckResult{}, //媒体安全检测结果
|
||||
//plant.MediaCheckResult{}, //媒体安全检测结果
|
||||
plant.Topic{}, //帖子话题
|
||||
plant.Post{}, //帖子
|
||||
plant.PostLike{}, //帖子点赞
|
||||
@@ -59,6 +61,10 @@ func MigrateTable() {
|
||||
plant.UserBadge{}, //用户徽章
|
||||
plant.UserStar{}, //用户收藏
|
||||
|
||||
plant.ExchangeItem{}, //兑换商品
|
||||
plant.ExchangeOrder{}, //兑换订单
|
||||
|
||||
plant.AiChatHistory{}, //AI问答历史
|
||||
)
|
||||
if err != nil {
|
||||
global.Logger.Error("Migrate table failed,err:", zap.Error(err))
|
||||
|
||||
@@ -46,6 +46,7 @@ func Routers() {
|
||||
systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关
|
||||
systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关
|
||||
systemRouter.InitOssRouter(NeedAuthGroup) //OSS相关
|
||||
systemRouter.InitSysAiConfigRouter(NeedAuthGroup) //AI配置相关
|
||||
}
|
||||
|
||||
{
|
||||
@@ -59,6 +60,9 @@ func Routers() {
|
||||
plantGroup.InitLevelConfigRouter(NeedAuthGroup) //等级配置
|
||||
plantGroup.InitBadgeConfigRouter(NeedAuthGroup) //徽章配置
|
||||
plantGroup.InitUserProfileRouter(NeedAuthGroup) //用户资料
|
||||
plantGroup.InitExchangeRouter(NeedAuthGroup) //兑换中心
|
||||
plantGroup.InitAiChatRouter(NeedAuthGroup) //AI聊天
|
||||
plantGroup.InitBannerRouter(NeedAuthGroup) //轮播图管理
|
||||
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ func InitTimer() {
|
||||
option = append(option, cron.WithSeconds())
|
||||
|
||||
// 任务一:每天8点30执行 发送植物养护提醒
|
||||
_, err := global.Timer.AddTaskByFuncWithSecond("SendCareRemind", "0 15 9 * * *", func() {
|
||||
_, err := global.Timer.AddTaskByFuncWithSecond("SendCareRemind", "0 30 8 * * *", func() {
|
||||
err1 := task.SendCareMsg()
|
||||
if err1 != nil {
|
||||
global.Logger.Error("定时发送植物养护提醒失败", zap.Error(err1))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package plant
|
||||
|
||||
import "sundynix-go/global"
|
||||
|
||||
// AiChatHistory AI问答历史记录
|
||||
type AiChatHistory struct {
|
||||
global.BaseModel
|
||||
UserId string `json:"userId" gorm:"size:50;index;comment:用户ID"`
|
||||
Question string `json:"question" gorm:"type:text;comment:用户提问"`
|
||||
Answer string `json:"answer" gorm:"type:longtext;comment:AI回答"`
|
||||
}
|
||||
|
||||
func (AiChatHistory) TableName() string {
|
||||
return "sundynix_ai_chat_history"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
type Banner struct {
|
||||
global.BaseModel
|
||||
Title string `json:"title" gorm:"column:title;comment:标题"`
|
||||
ImageId string `json:"imageId" gorm:"column:image_id;comment:图片ID"`
|
||||
Image *system.Oss `json:"image" gorm:"foreignKey:ImageId"`
|
||||
Sort int `json:"sort" gorm:"column:sort;default:0;comment:排序"`
|
||||
IsActive int `json:"isActive" gorm:"column:is_active;default:1;comment:状态 1:启用 2:禁用"`
|
||||
TargetUrl string `json:"targetUrl" gorm:"column:target_url;comment:跳转链接"`
|
||||
}
|
||||
|
||||
func (Banner) TableName() string {
|
||||
return "sundynix_plant_banner"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExchangeItem 兑换商品
|
||||
type ExchangeItem struct {
|
||||
global.BaseModel
|
||||
Name string `json:"name" gorm:"size:100;not null;column:name"` // 商品名称
|
||||
Description string `json:"description" gorm:"type:text;column:description"` // 商品描述
|
||||
ImageId string `json:"imageId" gorm:"size:50;column:image_id"` // 商品图片ID
|
||||
Type string `json:"type" gorm:"size:20;not null;default:PHYSICAL;column:type"` // 商品类型: PHYSICAL/VIRTUAL/COUPON
|
||||
CostSunlight int64 `json:"costSunlight" gorm:"not null;default:0;column:cost_sunlight"` // 消耗阳光值
|
||||
Stock int `json:"stock" gorm:"not null;default:-1;column:stock"` // 库存 -1无限
|
||||
LimitPerUser int `json:"limitPerUser" gorm:"not null;default:0;column:limit_per_user"` // 每人限兑次数 0不限
|
||||
Status int `json:"status" gorm:"not null;default:1;column:status"` // 1上架 2下架
|
||||
Sort int `json:"sort" gorm:"not null;default:0;column:sort"` // 排序
|
||||
StartTime *time.Time `json:"startTime" gorm:"column:start_time"` // 上架时间
|
||||
EndTime *time.Time `json:"endTime" gorm:"column:end_time"` // 下架时间
|
||||
Image *system.Oss `json:"image" gorm:"foreignKey:ImageId"` // 商品图片关联
|
||||
}
|
||||
|
||||
func (ExchangeItem) TableName() string {
|
||||
return "sundynix_exchange_item"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExchangeOrder 兑换订单
|
||||
type ExchangeOrder struct {
|
||||
global.BaseModel
|
||||
UserId string `json:"userId" gorm:"index;size:50;not null;column:user_id"` // 用户ID
|
||||
ItemId string `json:"itemId" gorm:"index;size:50;not null;column:item_id"` // 商品ID
|
||||
ItemName string `json:"itemName" gorm:"size:100;column:item_name"` // 商品名称(冗余快照)
|
||||
CostSunlight int64 `json:"costSunlight" gorm:"not null;default:0;column:cost_sunlight"` // 消耗阳光值
|
||||
Quantity int `json:"quantity" gorm:"not null;default:1;column:quantity"` // 数量
|
||||
Status int `json:"status" gorm:"not null;default:1;column:status"` // 1待处理 2处理中 3已发货 4已完成 5已取消
|
||||
ItemType string `json:"itemType" gorm:"size:20;column:item_type"` // 商品类型快照
|
||||
RecipientName string `json:"recipientName" gorm:"size:50;column:recipient_name"` // 收货人姓名
|
||||
Phone string `json:"phone" gorm:"size:20;column:phone"` // 联系电话
|
||||
Address string `json:"address" gorm:"size:255;column:address"` // 收货地址
|
||||
TrackingNo string `json:"trackingNo" gorm:"size:100;column:tracking_no"` // 快递单号
|
||||
Remark string `json:"remark" gorm:"size:255;column:remark"` // 备注
|
||||
CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"` // 完成时间
|
||||
Item *ExchangeItem `json:"item" gorm:"foreignKey:ItemId"` // 商品关联
|
||||
}
|
||||
|
||||
func (ExchangeOrder) TableName() string {
|
||||
return "sundynix_exchange_order"
|
||||
}
|
||||
|
||||
// 订单状态常量
|
||||
const (
|
||||
OrderStatusPending = 1 // 待处理
|
||||
OrderStatusProcessing = 2 // 处理中
|
||||
OrderStatusShipped = 3 // 已发货
|
||||
OrderStatusCompleted = 4 // 已完成
|
||||
OrderStatusCancelled = 5 // 已取消
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// MediaCheckResult 媒体安全检测结果
|
||||
// 对应微信 media_check_async 接口的返回结果
|
||||
type MediaCheckResult struct {
|
||||
global.BaseModel
|
||||
TraceId string `json:"traceId" gorm:"column:trace_id;size:100;uniqueIndex;comment:微信返回的唯一任务id"`
|
||||
PostId string `json:"postId" gorm:"column:post_id;size:50;index;comment:关联的帖子id"`
|
||||
OssId string `json:"ossId" gorm:"column:oss_id;size:50;comment:关联的oss文件id"`
|
||||
UserId string `json:"userId" gorm:"column:user_id;size:50;comment:提交检测的用户id"`
|
||||
Status int `json:"status" gorm:"column:status;default:0;comment:检测状态 0:检测中 1:通过 2:违规"`
|
||||
Type int `json:"type" gorm:"column:type;comment:媒体类型 1:音频 2:图片"`
|
||||
ErrMsg string `json:"errMsg" gorm:"column:err_msg;size:255;comment:错误信息"`
|
||||
}
|
||||
|
||||
func (MediaCheckResult) TableName() string {
|
||||
return "sundynix_media_check_result"
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/utils/timer"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CarePlan 养护计划
|
||||
@@ -19,28 +16,28 @@ type CarePlan struct {
|
||||
}
|
||||
|
||||
// AfterUpdate 钩子函数 修改计划后重新生成任务
|
||||
func (p *CarePlan) AfterUpdate(tx *gorm.DB) error {
|
||||
//1.删除旧任务
|
||||
err := tx.Where("plan_id = ?", p.Id).Unscoped().Delete(&CareTask{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//2.创建新任务
|
||||
today := timer.GetZeroTime()
|
||||
dueDate := today.AddDate(0, 0, p.Period)
|
||||
task := CareTask{
|
||||
UserId: p.UserId,
|
||||
PlantId: p.Id,
|
||||
PlanId: p.Id,
|
||||
Name: p.Name,
|
||||
Icon: p.Icon,
|
||||
DueDate: dueDate,
|
||||
Status: 1,
|
||||
}
|
||||
err = tx.Create(&task).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
// func (p *CarePlan) AfterUpdate(tx *gorm.DB) error {
|
||||
// //1.删除旧任务
|
||||
// err := tx.Where("plan_id = ?", p.Id).Unscoped().Delete(&CareTask{}).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// //2.创建新任务
|
||||
// today := timer.GetZeroTime()
|
||||
// dueDate := today.AddDate(0, 0, p.Period)
|
||||
// task := CareTask{
|
||||
// UserId: p.UserId,
|
||||
// PlantId: p.Id,
|
||||
// PlanId: p.Id,
|
||||
// Name: p.Name,
|
||||
// Icon: p.Icon,
|
||||
// DueDate: dueDate,
|
||||
// Status: 1,
|
||||
// }
|
||||
// err = tx.Create(&task).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package request
|
||||
|
||||
import "sundynix-go/model/commom/request"
|
||||
|
||||
type BannerPageReq struct {
|
||||
request.PageInfo
|
||||
Title string `json:"title" form:"title"`
|
||||
IsActive *int `json:"isActive" form:"isActive"` // 1 启用 2 禁用
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package request
|
||||
|
||||
import common "sundynix-go/model/commom/request"
|
||||
|
||||
// ExchangeItemCreateReq 创建兑换商品请求
|
||||
type ExchangeItemCreateReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
ImageId string `json:"imageId"`
|
||||
Type string `json:"type" binding:"required"` // PHYSICAL / VIRTUAL / COUPON
|
||||
CostSunlight int64 `json:"costSunlight" binding:"required,min=1"`
|
||||
Stock int `json:"stock"` // -1 无限
|
||||
LimitPerUser int `json:"limitPerUser"` // 0 不限
|
||||
Sort int `json:"sort"`
|
||||
StartTime string `json:"startTime"` // 可选
|
||||
EndTime string `json:"endTime"` // 可选
|
||||
}
|
||||
|
||||
// ExchangeItemUpdateReq 更新兑换商品请求
|
||||
type ExchangeItemUpdateReq struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ImageId string `json:"imageId"`
|
||||
Type string `json:"type"`
|
||||
CostSunlight int64 `json:"costSunlight"`
|
||||
Stock int `json:"stock"`
|
||||
LimitPerUser int `json:"limitPerUser"`
|
||||
Status int `json:"status"` // 1上架 2下架
|
||||
Sort int `json:"sort"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
}
|
||||
|
||||
// ExchangeItemListReq 商品列表查询请求
|
||||
type ExchangeItemListReq struct {
|
||||
common.PageInfo
|
||||
Type string `json:"type" form:"type"` // 按类型筛选
|
||||
Status int `json:"status" form:"status"` // 按状态筛选
|
||||
}
|
||||
|
||||
// ExchangeReq 用户兑换请求
|
||||
type ExchangeReq struct {
|
||||
ItemId string `json:"itemId" binding:"required"`
|
||||
Quantity int `json:"quantity"` // 默认1
|
||||
RecipientName string `json:"recipientName"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// ExchangeOrderListReq 订单列表查询请求
|
||||
type ExchangeOrderListReq struct {
|
||||
common.PageInfo
|
||||
Status int `json:"status" form:"status"` // 按状态筛选
|
||||
UserId string `json:"userId" form:"userId"` // 管理端按用户筛选
|
||||
}
|
||||
|
||||
// ExchangeOrderUpdateReq 更新订单状态请求 (管理端)
|
||||
type ExchangeOrderUpdateReq struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
Status int `json:"status" binding:"required"` // 目标状态
|
||||
TrackingNo string `json:"trackingNo"` // 快递单号(发货时)
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
@@ -63,3 +63,11 @@ type CreateGrowthRecord struct {
|
||||
Desc string `json:"desc"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// QuickCare 快捷养护记录(无需预设任务)
|
||||
type QuickCare struct {
|
||||
PlantId string `json:"plantId" binding:"required"`
|
||||
Name string `json:"name" binding:"required"` // 养护名称(如 浇水、施肥、喷雾...)
|
||||
Icon string `json:"icon"` // icon JSON(和 CarePlan 的 icon 格式一致)
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package response
|
||||
|
||||
// WeChatCheckResultCallback 微信内容安全回调数据结构
|
||||
type WeChatCheckResultCallback struct {
|
||||
ToUserName string `json:"ToUserName"`
|
||||
FromUserName string `json:"FromUserName"`
|
||||
CreateTime int64 `json:"CreateTime"`
|
||||
MsgType string `json:"MsgType"`
|
||||
Event string `json:"Event"`
|
||||
AppId string `json:"appid"`
|
||||
TraceId string `json:"trace_id"`
|
||||
Result struct {
|
||||
Suggest string `json:"suggest"` // pass, risky
|
||||
Label int `json:"label"`
|
||||
} `json:"result"`
|
||||
Detail []struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Errcode int `json:"errcode"`
|
||||
Suggest string `json:"suggest"`
|
||||
Label int `json:"label"`
|
||||
Prob int `json:"prob"`
|
||||
} `json:"detail"`
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type UserBadge struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"type:varchar(50);index;not null;column:user_id;comment:用户id" json:"userId"`
|
||||
BadgeId string `gorm:"index:idx_user_badge,unique;not null;column:badge_id;comment:徽章配置ID" json:"badgeId"`
|
||||
BadgeId string `gorm:"index:idx_user_badge;not null;column:badge_id;comment:徽章配置ID" json:"badgeId"`
|
||||
AcquiredAt time.Time `gorm:"autoCreateTime;column:acquired_at;comment:获得时间" json:"acquiredAt"`
|
||||
Badge *BadgeConfig `json:"badge" gorm:"foreignKey:BadgeId"`
|
||||
}
|
||||
|
||||
+4
-3
@@ -8,17 +8,18 @@ import (
|
||||
type Wiki struct {
|
||||
global.BaseModel
|
||||
IsHot int `json:"isHot" form:"isHot" gorm:"column:is_hot;comment:是否推荐植物"`
|
||||
IsVectorSynced int `json:"isVectorSynced" form:"isVectorSynced" gorm:"column:is_vector_synced;type:tinyint;default:0;comment:是否已同步到向量库(0否1是)"`
|
||||
//基本信息
|
||||
Name string `json:"name" form:"name" gorm:"column:name;size:50;comment:名称"`
|
||||
LatinName string `json:"latinName" form:"latinName" gorm:"size:100;column:latin_name;comment:拉丁名"`
|
||||
Aliases string `json:"aliases" form:"aliases" gorm:"size:100;column:aliases;comment:别名(逗号分隔)"`
|
||||
DistributionArea string `json:"distributionArea" form:"distributionArea" gorm:"size:100;column:distribution_area;comment:分布区域"` //分布区域
|
||||
DistributionArea string `json:"distributionArea" form:"distributionArea" gorm:"type:text;;column:distribution_area;comment:分布区域"` //分布区域
|
||||
//科学分类
|
||||
Genus string `json:"genus" form:"genus" gorm:"size:20;column:genus;comment:科属"` // 属
|
||||
Difficulty int `json:"difficulty" form:"difficulty" gorm:"column:difficulty;comment:种植难度"` //种植难度 1-5级
|
||||
//形态特征
|
||||
LifeCycle string `json:"lifeCycle" form:"lifeCycle" gorm:"size:20;column:life_cycle;comment:生命周期"` // 生命周期 一年生 二年生 多年生等
|
||||
GrowthHabit string `json:"growthHabit" form:"growthHabit" gorm:"size:200;column:growth_habit;comment:成长习性"` // 生长习性
|
||||
LifeCycle string `json:"lifeCycle" form:"lifeCycle" gorm:"type:text;column:life_cycle;comment:生命周期"` // 生命周期 一年生 二年生 多年生等
|
||||
GrowthHabit string `json:"growthHabit" form:"growthHabit" gorm:"type:text;column:growth_habit;comment:成长习性"` // 生长习性
|
||||
ReproductionMethod string `json:"reproductionMethod" form:"reproductionMethod" gorm:"size:200;column:reproduction_method;comment:繁殖方法"` //繁殖方法
|
||||
PestsDiseases string `json:"pestsDiseases" form:"pestsDiseases" gorm:"size:200;column:pests_diseases;comment:病虫害"`
|
||||
//光照
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package system
|
||||
|
||||
import "sundynix-go/global"
|
||||
|
||||
type SysAiConfig struct {
|
||||
global.BaseModel
|
||||
IsActive int `gorm:"column:is_active;type:tinyint;default:0;comment:是否激活(1是0否)" json:"isActive" form:"isActive"`
|
||||
// Qdrant 向量库配置
|
||||
QdrantUrl string `gorm:"column:qdrant_url;type:varchar(255);comment:Qdrant接口地址" json:"qdrantUrl" form:"qdrantUrl"`
|
||||
QdrantApiKey string `gorm:"column:qdrant_api_key;type:varchar(255);comment:Qdrant密钥" json:"qdrantApiKey" form:"qdrantApiKey"`
|
||||
QdrantCollection string `gorm:"column:qdrant_collection;type:varchar(100);comment:Qdrant集合名" json:"qdrantCollection" form:"qdrantCollection"`
|
||||
VectorDimension int `gorm:"column:vector_dimension;type:int;comment:向量维度(默认104)" json:"vectorDimension" form:"vectorDimension"`
|
||||
// 对话大模型配置(如 deepseek-chat、qwen-max、ollama 本地等)
|
||||
ChatProvider string `gorm:"column:chat_provider;type:varchar(50);comment:对话模型供应商(deepseek/qwen/local等)" json:"chatProvider" form:"chatProvider"`
|
||||
ChatApiUrl string `gorm:"column:chat_api_url;type:varchar(255);comment:对话模型接口地址" json:"chatApiUrl" form:"chatApiUrl"`
|
||||
ChatApiKey string `gorm:"column:chat_api_key;type:varchar(255);comment:对话模型ApiKey" json:"chatApiKey" form:"chatApiKey"`
|
||||
ChatModelName string `gorm:"column:chat_model_name;type:varchar(100);comment:对话模型名称" json:"chatModelName" form:"chatModelName"`
|
||||
// Embedding 向量化模型配置(可与对话模型用不同供应商,如 bge-m3 本地 + deepseek 对话)
|
||||
EmbeddingProvider string `gorm:"column:embedding_provider;type:varchar(50);comment:Embedding模型供应商" json:"embeddingProvider" form:"embeddingProvider"`
|
||||
EmbeddingApiUrl string `gorm:"column:embedding_api_url;type:varchar(255);comment:Embedding模型接口地址" json:"embeddingApiUrl" form:"embeddingApiUrl"`
|
||||
EmbeddingApiKey string `gorm:"column:embedding_api_key;type:varchar(255);comment:Embedding模型ApiKey" json:"embeddingApiKey" form:"embeddingApiKey"`
|
||||
EmbeddingModelName string `gorm:"column:embedding_model_name;type:varchar(100);comment:Embedding模型名称" json:"embeddingModelName" form:"embeddingModelName"`
|
||||
// 用量限制
|
||||
DailyQueryLimit int `gorm:"column:daily_query_limit;type:int;default:20;comment:每用户每日问答上限(0=不限)" json:"dailyQueryLimit" form:"dailyQueryLimit"`
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
package plant
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type AiChatRouter struct{}
|
||||
|
||||
func (s *AiChatRouter) InitAiChatRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
|
||||
aiChatRouter := Router.Group("plant/chat")
|
||||
aiChatApi := aiChatApi
|
||||
{
|
||||
aiChatRouter.GET("stream", aiChatApi.ChatStreamPlant) // SSE 对话流
|
||||
aiChatRouter.POST("sync", aiChatApi.SyncWikiToQdrant) // 后台知识库同步
|
||||
aiChatRouter.GET("history", aiChatApi.GetChatHistory) // 问答历史列表
|
||||
aiChatRouter.POST("history/delete", aiChatApi.DeleteChatHistory) // 删除单条
|
||||
aiChatRouter.POST("history/clear", aiChatApi.ClearChatHistory) // 清空历史
|
||||
aiChatRouter.GET("quota", aiChatApi.GetChatQuota) // 今日剩余额度
|
||||
}
|
||||
return aiChatRouter
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BannerRouter struct{}
|
||||
|
||||
func (c *BannerRouter) InitBannerRouter(Router *gin.RouterGroup) {
|
||||
bannerRouter := Router.Group("plantBanner")
|
||||
{
|
||||
bannerRouter.POST("create", bannerApi.Create)
|
||||
bannerRouter.POST("delete", bannerApi.Delete)
|
||||
bannerRouter.POST("update", bannerApi.Update)
|
||||
bannerRouter.POST("list", bannerApi.GetList)
|
||||
bannerRouter.GET("activeList", bannerApi.GetActiveList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CallbackRouter struct{}
|
||||
|
||||
func (s *CallbackRouter) InitCallbackRouter(Router *gin.RouterGroup) {
|
||||
callbackRouter := Router.Group("callback")
|
||||
{
|
||||
callbackRouter.POST("mediaCheck", callbackApi.MediaCheckCallback) // 接收微信媒体检测回调
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ type RouterGroup struct {
|
||||
BadgeConfigRouter
|
||||
UserProfileRouter
|
||||
CallbackRouter
|
||||
ExchangeRouter
|
||||
AiChatRouter
|
||||
BannerRouter
|
||||
}
|
||||
|
||||
// 初始化路由
|
||||
@@ -27,4 +30,7 @@ var (
|
||||
userProfileApi = v1.ApiGroupApp.PlantApiGroup.UserProfileApi
|
||||
badgeConfigApi = v1.ApiGroupApp.PlantApiGroup.BadgeConfigApi
|
||||
callbackApi = v1.ApiGroupApp.PlantApiGroup.CallbackApi
|
||||
exchangeApi = v1.ApiGroupApp.PlantApiGroup.ExchangeApi
|
||||
aiChatApi = v1.ApiGroupApp.PlantApiGroup.AiChatApi
|
||||
bannerApi = v1.ApiGroupApp.PlantApiGroup.BannerApi
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package plant
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type ExchangeRouter struct{}
|
||||
|
||||
func (r *ExchangeRouter) InitExchangeRouter(Router *gin.RouterGroup) {
|
||||
// ========== 用户端路由 ==========
|
||||
userRouter := Router.Group("exchange")
|
||||
{
|
||||
userRouter.GET("list", exchangeApi.UserItemList) // 商品列表
|
||||
userRouter.GET("detail", exchangeApi.UserItemDetail) // 商品详情
|
||||
userRouter.POST("redeem", exchangeApi.UserExchange) // 发起兑换
|
||||
userRouter.GET("orders", exchangeApi.UserOrderList) // 我的兑换记录
|
||||
}
|
||||
|
||||
// ========== 管理端路由 ==========
|
||||
adminRouter := Router.Group("exchange")
|
||||
{
|
||||
adminRouter.POST("item/create", exchangeApi.CreateItem) // 创建商品
|
||||
adminRouter.POST("item/update", exchangeApi.UpdateItem) // 更新商品
|
||||
adminRouter.POST("item/delete", exchangeApi.DeleteItem) // 删除商品
|
||||
adminRouter.POST("item/list", exchangeApi.AdminItemList) // 管理端商品列表
|
||||
adminRouter.POST("order/list", exchangeApi.AdminOrderList) // 管理端订单列表
|
||||
adminRouter.POST("order/update", exchangeApi.UpdateOrderStatus) // 更新订单状态
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ func (c *MyPlantRouter) InitPlantRouter(Router *gin.RouterGroup) {
|
||||
//成长记录
|
||||
myPlantRouter.POST("/growth/add", myPlantApi.AddGrowthRecord) // 添加成长记录
|
||||
|
||||
//快捷养护
|
||||
myPlantRouter.POST("quickCare", myPlantApi.QuickCare) // 快捷养护记录
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ func (c *UserProfileRouter) InitUserProfileRouter(Router *gin.RouterGroup) {
|
||||
userProfileRouter.GET("detail", userProfileApi.ProfileDetail)
|
||||
|
||||
userProfileRouter.POST("star", userProfileApi.MyStars) //我的收藏
|
||||
userProfileRouter.GET("badge", userProfileApi.MyBadges) //我的徽章
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ func (p *WikiRouter) InitWikiRouter(Router *gin.RouterGroup) {
|
||||
wikiRouter.POST("/update", wikiApi.UpdateWiki)
|
||||
wikiRouter.POST("/page", wikiApi.WikiPage)
|
||||
wikiRouter.GET("/detail", wikiApi.WikiDetail)
|
||||
wikiRouter.POST("/delete", wikiApi.DeleteWiki)
|
||||
wikiRouter.POST("/uploadImg", wikiApi.UploadImg)
|
||||
wikiRouter.POST("/sync-qdrant", wikiApi.SyncWikiQdrant)
|
||||
wikiRouter.POST("/delete-qdrant", wikiApi.DeleteWikiQdrant)
|
||||
|
||||
//用户端
|
||||
wikiRouter.GET("/star", wikiApi.StarWiki) //收藏或者取消收藏
|
||||
|
||||
@@ -10,6 +10,7 @@ type SysRouterGroup struct {
|
||||
MenuRouter
|
||||
OperationRecordRouter
|
||||
OssRouter
|
||||
SysAiConfigRouter
|
||||
}
|
||||
|
||||
// 初始化路由
|
||||
@@ -21,4 +22,5 @@ var (
|
||||
menuApi = v1.ApiGroupApp.SystemApiGroup.MenuApi
|
||||
operationRecordApi = v1.ApiGroupApp.SystemApiGroup.OperationRecordApi
|
||||
ossApi = v1.ApiGroupApp.SystemApiGroup.OssApi
|
||||
sysAiConfigApi = v1.ApiGroupApp.SystemApiGroup.SysAiConfigApi
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package system
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type SysAiConfigRouter struct{}
|
||||
|
||||
func (s *SysAiConfigRouter) InitSysAiConfigRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
|
||||
sysAiConfigRouter := Router.Group("aiConfig")
|
||||
sysAiConfigApi := sysAiConfigApi
|
||||
{
|
||||
sysAiConfigRouter.POST("create", sysAiConfigApi.CreateAiConfig) // 创建配置
|
||||
sysAiConfigRouter.POST("update", sysAiConfigApi.UpdateAiConfig) // 更新配置
|
||||
sysAiConfigRouter.POST("setActive", sysAiConfigApi.SetActive) // 设置激活状态
|
||||
sysAiConfigRouter.GET("list", sysAiConfigApi.GetList) // 获取列表
|
||||
}
|
||||
return sysAiConfigRouter
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
plantModel "sundynix-go/model/plant"
|
||||
)
|
||||
|
||||
type AiChatHistoryService struct{}
|
||||
|
||||
// SaveHistory 保存一条问答记录
|
||||
func (s *AiChatHistoryService) SaveHistory(userId, question, answer string) error {
|
||||
record := plantModel.AiChatHistory{
|
||||
UserId: userId,
|
||||
Question: question,
|
||||
Answer: answer,
|
||||
}
|
||||
return global.DB.Create(&record).Error
|
||||
}
|
||||
|
||||
// GetHistoryList 获取用户问答历史(分页,最新在前)
|
||||
func (s *AiChatHistoryService) GetHistoryList(userId string, current, pageSize int) ([]plantModel.AiChatHistory, int64, error) {
|
||||
var list []plantModel.AiChatHistory
|
||||
var total int64
|
||||
|
||||
db := global.DB.Model(&plantModel.AiChatHistory{}).Where("user_id = ?", userId)
|
||||
db.Count(&total)
|
||||
|
||||
if current <= 0 {
|
||||
current = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 50 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (current - 1) * pageSize
|
||||
|
||||
err := db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// DeleteHistory 删除单条记录
|
||||
func (s *AiChatHistoryService) DeleteHistory(userId, id string) error {
|
||||
return global.DB.Where("id = ? AND user_id = ?", id, userId).Delete(&plantModel.AiChatHistory{}).Error
|
||||
}
|
||||
|
||||
// ClearHistory 清空用户所有历史
|
||||
func (s *AiChatHistoryService) ClearHistory(userId string) error {
|
||||
return global.DB.Where("user_id = ?", userId).Delete(&plantModel.AiChatHistory{}).Error
|
||||
}
|
||||
|
||||
// GetTodayCount 获取用户今日问答数量
|
||||
func (s *AiChatHistoryService) GetTodayCount(userId string) (int64, error) {
|
||||
var count int64
|
||||
err := global.DB.Model(&plantModel.AiChatHistory{}).
|
||||
Where("user_id = ? AND DATE(created_at) = CURDATE()", userId).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
qdrant "github.com/qdrant/go-client/qdrant"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"sundynix-go/global"
|
||||
plantModel "sundynix-go/model/plant"
|
||||
"sundynix-go/model/system"
|
||||
systemService "sundynix-go/service/system"
|
||||
)
|
||||
|
||||
type AiRagService struct{}
|
||||
|
||||
var sysAiConfigService = systemService.SysAiConfigService{}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// OpenAI 客户端构建
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
func getChatClient(cfg *system.SysAiConfig) *openai.Client {
|
||||
config := openai.DefaultConfig(cfg.ChatApiKey)
|
||||
if cfg.ChatApiUrl != "" {
|
||||
config.BaseURL = cfg.ChatApiUrl
|
||||
}
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
func getEmbeddingClient(cfg *system.SysAiConfig) *openai.Client {
|
||||
config := openai.DefaultConfig(cfg.EmbeddingApiKey)
|
||||
if cfg.EmbeddingApiUrl != "" {
|
||||
config.BaseURL = cfg.EmbeddingApiUrl
|
||||
}
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Qdrant gRPC 连接
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
func newQdrantConn(cfg *system.SysAiConfig) (*grpc.ClientConn, context.Context, error) {
|
||||
addr := strings.TrimPrefix(cfg.QdrantUrl, "http://")
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("qdrant grpc dial failed: %w", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if cfg.QdrantApiKey != "" {
|
||||
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("api-key", cfg.QdrantApiKey))
|
||||
}
|
||||
return conn, ctx, nil
|
||||
}
|
||||
|
||||
// EnsureCollection 确保 Collection 存在,不存在则创建
|
||||
func EnsureCollection(cfg *system.SysAiConfig) error {
|
||||
conn, ctx, err := newQdrantConn(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
dim := uint64(cfg.VectorDimension)
|
||||
if dim == 0 {
|
||||
dim = 104
|
||||
}
|
||||
collClient := qdrant.NewCollectionsClient(conn)
|
||||
if _, getErr := collClient.Get(ctx, &qdrant.GetCollectionInfoRequest{CollectionName: cfg.QdrantCollection}); getErr == nil {
|
||||
return nil // 已存在
|
||||
}
|
||||
_, err = collClient.Create(ctx, &qdrant.CreateCollection{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
VectorsConfig: &qdrant.VectorsConfig{
|
||||
Config: &qdrant.VectorsConfig_Params{
|
||||
Params: &qdrant.VectorParams{Size: dim, Distance: qdrant.Distance_Cosine},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("qdrant create collection failed: %w", err)
|
||||
}
|
||||
global.Logger.Info("Qdrant collection created", zap.String("collection", cfg.QdrantCollection))
|
||||
return nil
|
||||
}
|
||||
|
||||
// wikiID → Qdrant point UUID(确保幂等)
|
||||
func wikiToQdrantID(wikiId string) string {
|
||||
return uuid.NewMD5(uuid.NameSpaceOID, []byte(wikiId)).String()
|
||||
}
|
||||
|
||||
// buildWikiText 拼接用于向量化的文本语料
|
||||
func buildWikiText(w plantModel.Wiki) string {
|
||||
return fmt.Sprintf(
|
||||
"植物名字:%s. 拉丁名:%s. 科属:%s. 生命周期:%s. 生长习性:%s. 病虫害:%s. 光照类型:%s. 最佳温度:%s.",
|
||||
w.Name, w.LatinName, w.Genus, w.LifeCycle, w.GrowthHabit,
|
||||
w.PestsDiseases, w.LightType, w.OptimalTempPeriod,
|
||||
)
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// SyncSingleWikiAsync 异步同步单条百科到 Qdrant(新增/更新时调用)
|
||||
// 同步成功后将 is_vector_synced 置为 1
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
func (s *AiRagService) SyncSingleWikiAsync(wikiId string) {
|
||||
go func() {
|
||||
if err := s.syncSingleWiki(wikiId); err != nil {
|
||||
global.Logger.Error("Async sync wiki to Qdrant failed", zap.String("wiki_id", wikiId), zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncSingleWiki 同步同步单条百科到 Qdrant(用于API直接调用)
|
||||
func (s *AiRagService) SyncSingleWiki(wikiId string) error {
|
||||
return s.syncSingleWiki(wikiId)
|
||||
}
|
||||
|
||||
func (s *AiRagService) syncSingleWiki(wikiId string) error {
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no active ai config: %w", err)
|
||||
}
|
||||
if err = EnsureCollection(cfg); err != nil {
|
||||
global.Logger.Warn("EnsureCollection warn", zap.Error(err))
|
||||
}
|
||||
|
||||
var w plantModel.Wiki
|
||||
if err = global.DB.Where("id = ?", wikiId).First(&w).Error; err != nil {
|
||||
return fmt.Errorf("wiki not found: %w", err)
|
||||
}
|
||||
|
||||
text := buildWikiText(w)
|
||||
embClient := getEmbeddingClient(cfg)
|
||||
embResp, err := embClient.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
|
||||
Input: []string{text},
|
||||
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("embedding failed: %w", err)
|
||||
}
|
||||
|
||||
conn, qdCtx, err := newQdrantConn(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
ptsClient := qdrant.NewPointsClient(conn)
|
||||
|
||||
_, err = ptsClient.Upsert(qdCtx, &qdrant.UpsertPoints{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
Points: []*qdrant.PointStruct{{
|
||||
Id: qdrant.NewID(wikiToQdrantID(wikiId)),
|
||||
Vectors: qdrant.NewVectors(embResp.Data[0].Embedding...),
|
||||
Payload: map[string]*qdrant.Value{
|
||||
"wiki_id": qdrant.NewValueString(w.Id),
|
||||
"name": qdrant.NewValueString(w.Name),
|
||||
"full_text": qdrant.NewValueString(text),
|
||||
},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("qdrant upsert failed: %w", err)
|
||||
}
|
||||
|
||||
// 更新同步状态
|
||||
_ = global.DB.Model(&plantModel.Wiki{}).Where("id = ?", wikiId).Update("is_vector_synced", 1).Error
|
||||
global.Logger.Info("Wiki synced to Qdrant", zap.String("wiki_id", wikiId))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// DeleteFromQdrant 从 Qdrant 删除单条植物的向量点位
|
||||
// 删除成功后将 is_vector_synced 置为 0
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
func (s *AiRagService) DeleteFromQdrant(wikiId string) error {
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no active ai config: %w", err)
|
||||
}
|
||||
conn, qdCtx, err := newQdrantConn(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
ptsClient := qdrant.NewPointsClient(conn)
|
||||
|
||||
qID := wikiToQdrantID(wikiId)
|
||||
_, err = ptsClient.Delete(qdCtx, &qdrant.DeletePoints{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
Points: &qdrant.PointsSelector{
|
||||
PointsSelectorOneOf: &qdrant.PointsSelector_Points{
|
||||
Points: &qdrant.PointsIdsList{
|
||||
Ids: []*qdrant.PointId{qdrant.NewID(qID)},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("qdrant delete failed: %w", err)
|
||||
}
|
||||
_ = global.DB.Model(&plantModel.Wiki{}).Where("id = ?", wikiId).Update("is_vector_synced", 0).Error
|
||||
global.Logger.Info("Wiki deleted from Qdrant", zap.String("wiki_id", wikiId))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFromQdrantBatch 批量从 Qdrant 删除(用于批量删除百科时)
|
||||
func (s *AiRagService) DeleteFromQdrantBatch(wikiIds []string) {
|
||||
go func() {
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err != nil {
|
||||
global.Logger.Warn("No active ai config for batch qdrant delete", zap.Error(err))
|
||||
return
|
||||
}
|
||||
conn, qdCtx, err := newQdrantConn(cfg)
|
||||
if err != nil {
|
||||
global.Logger.Warn("Qdrant connect failed for batch delete", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
ptsClient := qdrant.NewPointsClient(conn)
|
||||
|
||||
var ids []*qdrant.PointId
|
||||
for _, wid := range wikiIds {
|
||||
ids = append(ids, qdrant.NewID(wikiToQdrantID(wid)))
|
||||
}
|
||||
_, err = ptsClient.Delete(qdCtx, &qdrant.DeletePoints{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
Points: &qdrant.PointsSelector{
|
||||
PointsSelectorOneOf: &qdrant.PointsSelector_Points{
|
||||
Points: &qdrant.PointsIdsList{Ids: ids},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
global.Logger.Error("Qdrant batch delete failed", zap.Error(err))
|
||||
} else {
|
||||
global.Logger.Info("Qdrant batch delete done", zap.Int("count", len(wikiIds)))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// SyncWikiToQdrant 全量同步(后台操作/手动触发)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
func (s *AiRagService) SyncWikiToQdrant() error {
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = EnsureCollection(cfg); err != nil {
|
||||
global.Logger.Warn("EnsureCollection failed, continuing", zap.Error(err))
|
||||
}
|
||||
|
||||
var wikis []plantModel.Wiki
|
||||
if err = global.DB.Find(&wikis).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
embClient := getEmbeddingClient(cfg)
|
||||
conn, qdCtx, err := newQdrantConn(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
ptsClient := qdrant.NewPointsClient(conn)
|
||||
|
||||
var successIds []string
|
||||
for _, w := range wikis {
|
||||
text := buildWikiText(w)
|
||||
embResp, embErr := embClient.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
|
||||
Input: []string{text},
|
||||
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
|
||||
})
|
||||
if embErr != nil {
|
||||
global.Logger.Error("Embedding failed", zap.String("wiki_id", w.Id), zap.Error(embErr))
|
||||
continue
|
||||
}
|
||||
_, upsertErr := ptsClient.Upsert(qdCtx, &qdrant.UpsertPoints{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
Points: []*qdrant.PointStruct{{
|
||||
Id: qdrant.NewID(wikiToQdrantID(w.Id)),
|
||||
Vectors: qdrant.NewVectors(embResp.Data[0].Embedding...),
|
||||
Payload: map[string]*qdrant.Value{
|
||||
"wiki_id": qdrant.NewValueString(w.Id),
|
||||
"name": qdrant.NewValueString(w.Name),
|
||||
"full_text": qdrant.NewValueString(text),
|
||||
},
|
||||
}},
|
||||
})
|
||||
if upsertErr != nil {
|
||||
global.Logger.Error("Qdrant upsert failed", zap.String("wiki_id", w.Id), zap.Error(upsertErr))
|
||||
} else {
|
||||
successIds = append(successIds, w.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新同步状态
|
||||
if len(successIds) > 0 {
|
||||
_ = global.DB.Model(&plantModel.Wiki{}).Where("id IN ?", successIds).Update("is_vector_synced", 1).Error
|
||||
}
|
||||
global.Logger.Info("SyncWikiToQdrant done", zap.Int("total", len(wikis)), zap.Int("success", len(successIds)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// PlantChatStreamRAG 向量检索 + 大模型流式对话
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
func (s *AiRagService) PlantChatStreamRAG(ctx context.Context, userQuery string, onData func(chunk string) error) error {
|
||||
cfg, err := sysAiConfigService.GetActiveAiConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
embClient := getEmbeddingClient(cfg)
|
||||
chatClient := getChatClient(cfg)
|
||||
|
||||
embResp, err := embClient.CreateEmbeddings(ctx, openai.EmbeddingRequest{
|
||||
Input: []string{userQuery},
|
||||
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("向量化查询失败: %w", err)
|
||||
}
|
||||
|
||||
conn, qdCtx, connErr := newQdrantConn(cfg)
|
||||
var contextText string
|
||||
if connErr == nil {
|
||||
defer conn.Close()
|
||||
ptsClient := qdrant.NewPointsClient(conn)
|
||||
limit := uint64(3)
|
||||
searchRes, searchErr := ptsClient.Search(qdCtx, &qdrant.SearchPoints{
|
||||
CollectionName: cfg.QdrantCollection,
|
||||
Vector: embResp.Data[0].Embedding,
|
||||
Limit: limit,
|
||||
WithPayload: &qdrant.WithPayloadSelector{
|
||||
SelectorOptions: &qdrant.WithPayloadSelector_Enable{Enable: true},
|
||||
},
|
||||
})
|
||||
if searchErr != nil {
|
||||
global.Logger.Warn("Qdrant search failed, using empty context", zap.Error(searchErr))
|
||||
} else {
|
||||
for _, pt := range searchRes.GetResult() {
|
||||
if txt, ok := pt.GetPayload()["full_text"]; ok {
|
||||
contextText += txt.GetStringValue() + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
global.Logger.Warn("Qdrant connect failed, skipping RAG", zap.Error(connErr))
|
||||
}
|
||||
|
||||
systemPrompt := "你是一个专业的植物百科助手。回答规则:1.基于知识库信息回答,不够则结合通用知识。2.严禁使用Markdown语法(不要用#、*、-、```等符号)。3.用纯文本回答,段落之间空一行。4.分类用「一、二、三」或emoji开头,重点内容直接加书名号《》或【】强调。5.回答简洁专业、条理清晰。\n"
|
||||
if contextText != "" {
|
||||
systemPrompt += "--- 知识库 ---\n" + contextText + "\n--------------\n"
|
||||
}
|
||||
|
||||
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
|
||||
Model: cfg.ChatModelName,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
|
||||
{Role: openai.ChatMessageRoleUser, Content: userQuery},
|
||||
},
|
||||
Stream: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("大模型调用失败: %w", err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if errors.Is(recvErr, io.EOF) {
|
||||
break
|
||||
}
|
||||
if recvErr != nil {
|
||||
return recvErr
|
||||
}
|
||||
if len(resp.Choices) > 0 {
|
||||
if content := resp.Choices[0].Delta.Content; content != "" {
|
||||
if writeErr := onData(content); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/plant"
|
||||
"sundynix-go/model/plant/request"
|
||||
)
|
||||
|
||||
type BannerService struct{}
|
||||
|
||||
func (s *BannerService) Create(req plant.Banner) error {
|
||||
return global.DB.Create(&req).Error
|
||||
}
|
||||
|
||||
func (s *BannerService) Delete(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&plant.Banner{}).Error
|
||||
}
|
||||
|
||||
func (s *BannerService) Update(req plant.Banner) error {
|
||||
return global.DB.Model(&plant.Banner{}).Where("id = ?", req.Id).Updates(map[string]interface{}{
|
||||
"title": req.Title,
|
||||
"image_id": req.ImageId,
|
||||
"sort": req.Sort,
|
||||
"is_active": req.IsActive,
|
||||
"target_url": req.TargetUrl,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *BannerService) GetList(req request.BannerPageReq) (list interface{}, total int64, err error) {
|
||||
limit := req.PageSize
|
||||
offset := req.PageSize * (req.Current - 1)
|
||||
db := global.DB.Model(&plant.Banner{}).Preload("Image")
|
||||
if req.Title != "" {
|
||||
db = db.Where("title LIKE ?", "%"+req.Title+"%")
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
db = db.Where("is_active = ?", *req.IsActive)
|
||||
}
|
||||
|
||||
var banners []plant.Banner
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Order("sort asc, created_at desc").Limit(limit).Offset(offset).Find(&banners).Error
|
||||
return banners, total, err
|
||||
}
|
||||
|
||||
func (s *BannerService) GetActiveList() ([]plant.Banner, error) {
|
||||
var banners []plant.Banner
|
||||
err := global.DB.Model(&plant.Banner{}).Preload("Image").
|
||||
Where("is_active = 1").
|
||||
Order("sort asc, created_at desc").
|
||||
Find(&banners).Error
|
||||
return banners, err
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/plant"
|
||||
plantres "sundynix-go/model/plant/response"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CallbackService struct{}
|
||||
|
||||
var CallbackServiceApp = new(CallbackService)
|
||||
|
||||
// HandleMediaCheckCallback 处理媒体检测回调
|
||||
func (s *CallbackService) HandleMediaCheckCallback(cb plantres.WeChatCheckResultCallback) error {
|
||||
global.Logger.Info("收到微信媒体检测回调", zap.String("traceId", cb.TraceId), zap.String("suggest", cb.Result.Suggest))
|
||||
|
||||
var checkResult plant.MediaCheckResult
|
||||
err := global.DB.Where("trace_id = ?", cb.TraceId).First(&checkResult).Error
|
||||
if err != nil {
|
||||
global.Logger.Error("回调traceId未找到", zap.String("traceId", cb.TraceId), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 更新检测结果状态
|
||||
status := 0
|
||||
if cb.Result.Suggest == "pass" {
|
||||
status = 1
|
||||
} else {
|
||||
status = 2
|
||||
}
|
||||
|
||||
err = global.DB.Model(&checkResult).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"err_msg": cb.Result.Suggest,
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
global.Logger.Error("更新检测结果失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 根据结果处理帖子状态
|
||||
return s.updatePostStatus(checkResult.PostId)
|
||||
}
|
||||
|
||||
// updatePostStatus 更新帖子状态
|
||||
// 逻辑:
|
||||
// 1. 如果有任意一个检测结果为违规(2) -> 帖子违规(2)
|
||||
// 2. 如果所有检测结果都为通过(1) -> 帖子通过(1)
|
||||
// 3. 否则保持待审核(0)
|
||||
func (s *CallbackService) updatePostStatus(postId string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var post plant.Post
|
||||
if err := tx.Where("id = ?", postId).First(&post).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果帖子已经是违规状态,无需再处理(可能之前已经由文本检测判定违规)
|
||||
if post.HasReviewed == 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []plant.MediaCheckResult
|
||||
if err := tx.Where("post_id = ?", postId).Find(&results).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasRisky := false
|
||||
allPass := true
|
||||
|
||||
for _, res := range results {
|
||||
if res.Status == 2 {
|
||||
hasRisky = true
|
||||
break
|
||||
}
|
||||
if res.Status != 1 {
|
||||
allPass = false
|
||||
}
|
||||
}
|
||||
|
||||
var newStatus = post.HasReviewed // 默认保持原状态
|
||||
|
||||
if hasRisky {
|
||||
newStatus = 2
|
||||
// TODO: 这里可以执行额外的封禁逻辑,例如不仅标记违规,还软删除Oss关联等
|
||||
global.Logger.Warn("帖子包含违规图片,标记为违规", zap.String("postId", postId))
|
||||
} else if allPass {
|
||||
// 只有当所有图片都通过,且原状态不是违规时,才标记为通过
|
||||
// 注意:这里假设文本检测已经通过(文本检测是同步的,若不通过早已设为2)
|
||||
// 如果文本检测尚未完成(理论上不可能,因为是先文本后图片),这里可能会有竞态,但文本检测在发帖goroutine中是串行的。
|
||||
// 唯一需要注意的是,如果文本检测还在进行中,这里不应覆盖。
|
||||
// 但我们在PublishPost中是先改HasReviewed再发图片检查。
|
||||
// 如果文本通过,HasReviewed会被设为1? 不,根据新逻辑,PublishPost中只有无图才设为1。
|
||||
// 有图时,PublishPost中HasReviewed保持0。
|
||||
newStatus = 1
|
||||
global.Logger.Info("帖子所有图片检测通过,标记为通过", zap.String("postId", postId))
|
||||
}
|
||||
|
||||
if newStatus != post.HasReviewed {
|
||||
if err := tx.Model(&post).Update("has_reviewed", newStatus).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -11,4 +11,8 @@ type ServiceGroup struct {
|
||||
BadgeConfigService
|
||||
UserProfileService
|
||||
CallbackService
|
||||
ExchangeService
|
||||
AiRagService
|
||||
AiChatHistoryService
|
||||
BannerService
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package plant
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/plant"
|
||||
plantReq "sundynix-go/model/plant/request"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ExchangeService struct{}
|
||||
|
||||
// ==================== 管理端 ====================
|
||||
|
||||
// CreateItem 创建兑换商品
|
||||
func (s *ExchangeService) CreateItem(req plantReq.ExchangeItemCreateReq) error {
|
||||
item := plant.ExchangeItem{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
ImageId: req.ImageId,
|
||||
Type: req.Type,
|
||||
CostSunlight: req.CostSunlight,
|
||||
Stock: req.Stock,
|
||||
LimitPerUser: req.LimitPerUser,
|
||||
Sort: req.Sort,
|
||||
Status: 1, // 默认上架
|
||||
}
|
||||
if req.StartTime != "" {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", req.StartTime)
|
||||
item.StartTime = &t
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", req.EndTime)
|
||||
item.EndTime = &t
|
||||
}
|
||||
return global.DB.Create(&item).Error
|
||||
}
|
||||
|
||||
// UpdateItem 更新兑换商品
|
||||
func (s *ExchangeService) UpdateItem(req plantReq.ExchangeItemUpdateReq) error {
|
||||
updateMap := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"image_id": req.ImageId,
|
||||
"type": req.Type,
|
||||
"cost_sunlight": req.CostSunlight,
|
||||
"stock": req.Stock,
|
||||
"limit_per_user": req.LimitPerUser,
|
||||
"status": req.Status,
|
||||
"sort": req.Sort,
|
||||
}
|
||||
if req.StartTime != "" {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", req.StartTime)
|
||||
updateMap["start_time"] = t
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", req.EndTime)
|
||||
updateMap["end_time"] = t
|
||||
}
|
||||
return global.DB.Model(&plant.ExchangeItem{}).Where("id = ?", req.Id).Updates(updateMap).Error
|
||||
}
|
||||
|
||||
// DeleteItem 删除兑换商品
|
||||
func (s *ExchangeService) DeleteItem(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&plant.ExchangeItem{}).Error
|
||||
}
|
||||
|
||||
// AdminItemList 管理端商品列表(含下架)
|
||||
func (s *ExchangeService) AdminItemList(req plantReq.ExchangeItemListReq) (list []plant.ExchangeItem, total int64, err error) {
|
||||
db := global.DB.Model(&plant.ExchangeItem{}).Preload("Image")
|
||||
if req.Type != "" {
|
||||
db = db.Where("type = ?", req.Type)
|
||||
}
|
||||
if req.Status != 0 {
|
||||
db = db.Where("status = ?", req.Status)
|
||||
}
|
||||
if req.Keyword != "" {
|
||||
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Scopes(req.Paginate()).Order("sort asc, created_at desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// AdminOrderList 管理端订单列表
|
||||
func (s *ExchangeService) AdminOrderList(req plantReq.ExchangeOrderListReq) (list []plant.ExchangeOrder, total int64, err error) {
|
||||
db := global.DB.Model(&plant.ExchangeOrder{}).Preload("Item", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Image")
|
||||
})
|
||||
if req.Status != 0 {
|
||||
db = db.Where("status = ?", req.Status)
|
||||
}
|
||||
if req.UserId != "" {
|
||||
db = db.Where("user_id = ?", req.UserId)
|
||||
}
|
||||
if req.Keyword != "" {
|
||||
db = db.Where("item_name LIKE ? OR recipient_name LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Scopes(req.Paginate()).Order("created_at desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateOrderStatus 更新订单状态(管理端)
|
||||
func (s *ExchangeService) UpdateOrderStatus(req plantReq.ExchangeOrderUpdateReq) error {
|
||||
updateMap := map[string]interface{}{
|
||||
"status": req.Status,
|
||||
}
|
||||
if req.TrackingNo != "" {
|
||||
updateMap["tracking_no"] = req.TrackingNo
|
||||
}
|
||||
if req.Remark != "" {
|
||||
updateMap["remark"] = req.Remark
|
||||
}
|
||||
if req.Status == plant.OrderStatusCompleted {
|
||||
now := time.Now()
|
||||
updateMap["completed_at"] = now
|
||||
}
|
||||
// 如果取消订单,退还阳光值
|
||||
if req.Status == plant.OrderStatusCancelled {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var order plant.ExchangeOrder
|
||||
if err := tx.Where("id = ?", req.Id).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status == plant.OrderStatusCancelled {
|
||||
return errors.New("订单已取消")
|
||||
}
|
||||
// 退还阳光值
|
||||
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", order.UserId).
|
||||
Update("current_sunlight", gorm.Expr("current_sunlight + ?", order.CostSunlight)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 恢复库存
|
||||
if err := tx.Model(&plant.ExchangeItem{}).Where("id = ? AND stock >= 0", order.ItemId).
|
||||
Update("stock", gorm.Expr("stock + ?", order.Quantity)).Error; err != nil {
|
||||
// 库存为-1(无限)时,WHERE条件不匹配,不影响
|
||||
}
|
||||
return tx.Model(&plant.ExchangeOrder{}).Where("id = ?", req.Id).Updates(updateMap).Error
|
||||
})
|
||||
}
|
||||
return global.DB.Model(&plant.ExchangeOrder{}).Where("id = ?", req.Id).Updates(updateMap).Error
|
||||
}
|
||||
|
||||
// ==================== 用户端 ====================
|
||||
|
||||
// UserItemList 用户端商品列表(仅上架)
|
||||
func (s *ExchangeService) UserItemList(req plantReq.ExchangeItemListReq) (list []plant.ExchangeItem, total int64, err error) {
|
||||
db := global.DB.Model(&plant.ExchangeItem{}).Preload("Image").
|
||||
Where("status = 1")
|
||||
if req.Type != "" {
|
||||
db = db.Where("type = ?", req.Type)
|
||||
}
|
||||
if req.Keyword != "" {
|
||||
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Scopes(req.Paginate()).Order("sort asc, created_at desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// UserItemDetail 商品详情
|
||||
func (s *ExchangeService) UserItemDetail(itemId string) (plant.ExchangeItem, error) {
|
||||
var item plant.ExchangeItem
|
||||
err := global.DB.Preload("Image").Where("id = ? AND status = 1", itemId).First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
// UserExchange 用户发起兑换
|
||||
func (s *ExchangeService) UserExchange(req plantReq.ExchangeReq, userId string) error {
|
||||
if req.Quantity <= 0 {
|
||||
req.Quantity = 1
|
||||
}
|
||||
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 查询并锁定商品
|
||||
var item plant.ExchangeItem
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
Where("id = ? AND status = 1", req.ItemId).First(&item).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("商品不存在或已下架")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查有效期
|
||||
now := time.Now()
|
||||
if item.StartTime != nil && now.Before(*item.StartTime) {
|
||||
return errors.New("兑换尚未开始")
|
||||
}
|
||||
if item.EndTime != nil && now.After(*item.EndTime) {
|
||||
return errors.New("兑换已结束")
|
||||
}
|
||||
|
||||
// 3. 检查库存
|
||||
if item.Stock >= 0 && item.Stock < req.Quantity {
|
||||
return errors.New("库存不足")
|
||||
}
|
||||
|
||||
// 4. 检查每人限兑
|
||||
if item.LimitPerUser > 0 {
|
||||
var count int64
|
||||
tx.Model(&plant.ExchangeOrder{}).Where("user_id = ? AND item_id = ? AND status != ?",
|
||||
userId, req.ItemId, plant.OrderStatusCancelled).Count(&count)
|
||||
if int(count)+req.Quantity > item.LimitPerUser {
|
||||
return errors.New("已达到兑换上限")
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 计算总消耗
|
||||
totalCost := item.CostSunlight * int64(req.Quantity)
|
||||
|
||||
// 6. 扣减阳光值
|
||||
result := tx.Model(&plant.UserProfile{}).
|
||||
Where("user_id = ? AND current_sunlight >= ?", userId, totalCost).
|
||||
Update("current_sunlight", gorm.Expr("current_sunlight - ?", totalCost))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("阳光值不足")
|
||||
}
|
||||
|
||||
// 7. 扣减库存
|
||||
if item.Stock >= 0 {
|
||||
if err := tx.Model(&plant.ExchangeItem{}).Where("id = ? AND stock >= ?", item.Id, req.Quantity).
|
||||
Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 创建订单
|
||||
order := plant.ExchangeOrder{
|
||||
UserId: userId,
|
||||
ItemId: item.Id,
|
||||
ItemName: item.Name,
|
||||
CostSunlight: totalCost,
|
||||
Quantity: req.Quantity,
|
||||
Status: plant.OrderStatusPending,
|
||||
ItemType: item.Type,
|
||||
RecipientName: req.RecipientName,
|
||||
Phone: req.Phone,
|
||||
Address: req.Address,
|
||||
}
|
||||
|
||||
// 虚拟商品自动完成
|
||||
if item.Type == "VIRTUAL" {
|
||||
order.Status = plant.OrderStatusCompleted
|
||||
now := time.Now()
|
||||
order.CompletedAt = &now
|
||||
}
|
||||
|
||||
return tx.Create(&order).Error
|
||||
})
|
||||
}
|
||||
|
||||
// UserOrderList 用户订单列表
|
||||
func (s *ExchangeService) UserOrderList(req plantReq.ExchangeOrderListReq, userId string) (list []plant.ExchangeOrder, total int64, err error) {
|
||||
db := global.DB.Model(&plant.ExchangeOrder{}).Preload("Item", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Image")
|
||||
}).Where("user_id = ?", userId)
|
||||
if req.Status != 0 {
|
||||
db = db.Where("status = ?", req.Status)
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Scopes(req.Paginate()).Order("created_at desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (s *MyPlantService) UpdatePlant(req plantReq.UpdateMyPlant) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//3.重新生成任务 CarePlans 结构体中使用钩子函数自动执行
|
||||
//3.重新生成任务
|
||||
//3.1 删除旧任务
|
||||
err = tx.Where("plan_id = ?", plan.Id).Unscoped().Delete(&plant.CareTask{}).Error
|
||||
if err != nil {
|
||||
@@ -177,6 +177,7 @@ func (s *MyPlantService) UpdatePlant(req plantReq.UpdateMyPlant) error {
|
||||
Icon: plan.Icon,
|
||||
DueDate: dueDate,
|
||||
Status: 1,
|
||||
TargetAction: plan.TargetAction,
|
||||
}
|
||||
err = tx.Create(&task).Error
|
||||
if err != nil {
|
||||
@@ -551,6 +552,71 @@ func (s *MyPlantService) AddGrowthRecord(req plantReq.CreateGrowthRecord, userId
|
||||
})
|
||||
}
|
||||
|
||||
// QuickCare 快捷养护记录(无需预设任务,直接创建养护记录)
|
||||
func (s *MyPlantService) QuickCare(req plantReq.QuickCare, userId string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1.验证植物存在且属于该用户
|
||||
var myPlant plant.MyPlant
|
||||
if err := tx.Where("id = ? AND user_id = ?", req.PlantId, userId).First(&myPlant).Error; err != nil {
|
||||
return errors.New("植物不存在")
|
||||
}
|
||||
// 2.直接创建养护记录
|
||||
record := plant.CareRecord{
|
||||
UserId: userId,
|
||||
PlantId: req.PlantId,
|
||||
Name: req.Name,
|
||||
Icon: req.Icon,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 3.更新用户 profile 对应计数(尝试从 icon JSON 解析 targetAction)
|
||||
column := "care_count"
|
||||
if req.Icon != "" {
|
||||
// 解析 icon JSON 中的 id 字段来判断动作类型
|
||||
actionMap := map[string]string{
|
||||
"water": "water_count", "fertilize": "fertilize_count",
|
||||
"prune": "prune_count", "repot": "repot_count",
|
||||
}
|
||||
// 简单提取: 不引入 encoding/json 解析,用名称匹配
|
||||
nameMap := map[string]string{
|
||||
"浇水": "water_count", "施肥": "fertilize_count",
|
||||
"修剪": "prune_count", "换盆": "repot_count",
|
||||
}
|
||||
if col, ok := nameMap[req.Name]; ok {
|
||||
column = col
|
||||
} else {
|
||||
// 尝试从 actionMap (icon.id) 匹配
|
||||
for key, col := range actionMap {
|
||||
if len(req.Icon) > 0 && contains(req.Icon, key) {
|
||||
column = col
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", userId).
|
||||
Update(column, gorm.Expr(column+" + 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
|
||||
}
|
||||
|
||||
func containsStr(s, sub string) bool {
|
||||
for i := 0; i <= len(s)-len(sub); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 完成任务后异步执行 更新用户等级 sunlight
|
||||
func (s *MyPlantService) handleCompleteTaskThenUpdateProfile(ctx context.Context, userId string) {
|
||||
//5.更新用户profile
|
||||
|
||||
@@ -155,6 +155,8 @@ func (s *PostService) PostPage(req plantReq.PostPage, userId string) (list inter
|
||||
return db.Preload("Commentator", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Avatar")
|
||||
})
|
||||
}).Preload("LikeList", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Liker")
|
||||
})
|
||||
var posts []plant.Post
|
||||
if req.Title != "" {
|
||||
|
||||
@@ -39,7 +39,10 @@ func (s *UserProfileService) UpdateProfile(req plantReq.UpdateProfile, userId st
|
||||
// ProfileDetail 获取用户详情
|
||||
func (s *UserProfileService) ProfileDetail(userId string) (plant.UserProfile, error) {
|
||||
var res plant.UserProfile
|
||||
err := global.DB.Where("user_id = ?", userId).Preload("Avatar").Preload("Level").First(&res).Error
|
||||
err := global.DB.Where("user_id = ?", userId).
|
||||
Preload("Avatar").
|
||||
Preload("Level").
|
||||
First(&res).Error
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -71,3 +74,14 @@ func (s *UserProfileService) MyStars(req plantReq.StarsPageReq, userId string) (
|
||||
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&stars).Error
|
||||
return stars, total, err
|
||||
}
|
||||
|
||||
// MyBadges 我的徽章
|
||||
func (s *UserProfileService) MyBadges(userId string) ([]plant.UserBadge, error) {
|
||||
var badges []plant.UserBadge
|
||||
err := global.DB.Where("user_id = ?", userId).
|
||||
Preload("Badge", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Icon")
|
||||
}).
|
||||
Find(&badges).Error
|
||||
return badges, err
|
||||
}
|
||||
|
||||
+71
-2
@@ -3,6 +3,7 @@ package plant
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
"sundynix-go/model/plant"
|
||||
plantReq "sundynix-go/model/plant/request"
|
||||
"sundynix-go/model/system"
|
||||
@@ -10,13 +11,15 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var aiRagService = AiRagService{}
|
||||
|
||||
type WikiService struct{}
|
||||
|
||||
var WikiServiceApp = new(WikiClassService)
|
||||
|
||||
// CreateWiki 创建百科
|
||||
func (s *WikiService) CreateWiki(req plantReq.CreateWiki) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
//1.先模糊查询name是否存在 如果存在 则返回错误
|
||||
if !errors.Is(tx.Where("name like ?", "%"+req.Name+"%").First(&plant.Wiki{}).Error, gorm.ErrRecordNotFound) {
|
||||
return errors.New("植物已经存在")
|
||||
@@ -116,6 +119,14 @@ func (s *WikiService) CreateWiki(req plantReq.CreateWiki) error {
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
// 异步同步到 Qdrant(事务提交后,wiki.Id 已可用)
|
||||
var created plant.Wiki
|
||||
if dbErr := global.DB.Where("name = ?", req.Name).First(&created).Error; dbErr == nil {
|
||||
aiRagService.SyncSingleWikiAsync(created.Id)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateWiki 修改百科
|
||||
@@ -172,7 +183,7 @@ func (s *WikiService) WikiPage(req plantReq.WikiPage, userId string) (list inter
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&wikis).Error
|
||||
err = db.Limit(limit).Offset(offset).Order("is_hot desc,created_at desc").Find(&wikis).Error
|
||||
|
||||
// 优化 N+1 查询
|
||||
var wikiIds []string
|
||||
@@ -258,3 +269,61 @@ func (s *WikiService) StarWiki(userId, wikiId, class string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadImg 上传图片
|
||||
func (s *WikiService) UploadImg(req common.UploadOss) error {
|
||||
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var wiki plant.Wiki
|
||||
err := tx.Where("id = ?", req.Id).First(&wiki).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ossList []system.Oss
|
||||
err = tx.Where("id in ?", req.OssIds).Find(&ossList).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var relations []map[string]interface{}
|
||||
for _, oss := range ossList {
|
||||
relations = append(relations, map[string]interface{}{
|
||||
"wiki_id": wiki.Id,
|
||||
"oss_id": oss.Id,
|
||||
})
|
||||
}
|
||||
//3.添加关联关系(添加引用)
|
||||
return global.DB.Table("sundynix_wiki_oss").Create(relations).Error
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteWiki 删除百科
|
||||
func (s *WikiService) DeleteWiki(req common.IdsReq) error {
|
||||
err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var imgIds []string
|
||||
tx.Table("sundynix_wiki_oss").Where("wiki_id IN ?", req.Ids).Pluck("oss_id", &imgIds)
|
||||
// 3. 物理删除图片记录本身
|
||||
if len(imgIds) > 0 {
|
||||
if err := tx.Unscoped().Where("id IN ?", imgIds).Delete(&system.Oss{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 2. 清理中间表记录 (解开多对多关系)
|
||||
// 使用 Exec 直接操作中间表比循环 Clear 快得多
|
||||
if err := tx.Exec("DELETE FROM sundynix_wiki_oss WHERE wiki_id IN ?", req.Ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec("DELETE FROM sundynix_wiki_class WHERE wiki_id IN ?", req.Ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec("DELETE FROM sundynix_wiki_related WHERE wiki_id IN ? or related_wiki_id in ?", req.Ids, req.Ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
//删除百科本身
|
||||
return tx.Unscoped().Where("id IN ?", req.Ids).Delete(&plant.Wiki{}).Error
|
||||
})
|
||||
if err == nil {
|
||||
// 异步清理 Qdrant 向量点位
|
||||
aiRagService.DeleteFromQdrantBatch(req.Ids)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ type ServiceGroup struct {
|
||||
MenuService
|
||||
OperationRecordService
|
||||
OssService
|
||||
SysAiConfigService
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go.uber.org/zap"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
type SysAiConfigService struct{}
|
||||
|
||||
// Create 创建AI配置
|
||||
func (s *SysAiConfigService) Create(cfg *system.SysAiConfig) error {
|
||||
return global.DB.Create(cfg).Error
|
||||
}
|
||||
|
||||
// Update 更新AI配置
|
||||
func (s *SysAiConfigService) Update(cfg *system.SysAiConfig) error {
|
||||
return global.DB.Updates(cfg).Error
|
||||
}
|
||||
|
||||
// SetActive 设置激活配置
|
||||
func (s *SysAiConfigService) SetActive(id string) error {
|
||||
// 先将所有的设为 0
|
||||
err := global.DB.Model(&system.SysAiConfig{}).Where("1 = 1").Update("is_active", 0).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 再将指定的设为 1
|
||||
return global.DB.Model(&system.SysAiConfig{}).Where("id = ?", id).Update("is_active", 1).Error
|
||||
}
|
||||
|
||||
// GetActiveAiConfig 获取当前激活的AI配置
|
||||
func (s *SysAiConfigService) GetActiveAiConfig() (*system.SysAiConfig, error) {
|
||||
var cfg system.SysAiConfig
|
||||
err := global.DB.Where("is_active = 1").First(&cfg).Error
|
||||
if err != nil {
|
||||
global.Logger.Error("No active AI Config found", zap.Error(err))
|
||||
return nil, errors.New("无激活状态的AI配置")
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// GetList 获取所有配置
|
||||
func (s *SysAiConfigService) GetList() ([]system.SysAiConfig, error) {
|
||||
var list []system.SysAiConfig
|
||||
err := global.DB.Order("created_at desc").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (userService *UserService) GetUserList(info systemReq.GetUserList) (list in
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Find(&userList).Error
|
||||
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&userList).Error
|
||||
return userList, total, err
|
||||
}
|
||||
|
||||
@@ -162,11 +162,17 @@ func (userService *UserService) MiniLogin(code string) (result *system.User, err
|
||||
UserId: newUser.Id,
|
||||
MiniOpenId: wxResp.Openid,
|
||||
Nickname: newUser.Name,
|
||||
LevelId: "9bc0cd78-070f-11f1-8e90-1a43c43655d1",
|
||||
CurrentSunlight: 0,
|
||||
TotalSunlight: 0,
|
||||
PlantCount: 0,
|
||||
CareCount: 0,
|
||||
PostCount: 0,
|
||||
WaterCount: 0,
|
||||
FertilizeCount: 0,
|
||||
RepotCount: 0,
|
||||
PruneCount: 0,
|
||||
PhotoCount: 0,
|
||||
}
|
||||
if err := tx.Create(&profile).Error; err != nil {
|
||||
return err
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -47,8 +47,11 @@ func SendCareMsg() error {
|
||||
// 将tasks分组,key为用户id 保证无论用户有多少植物,只给用户发送一条消息
|
||||
tasksMap := make(map[string][]*plant.CareTask)
|
||||
for _, task := range tasks {
|
||||
//用户id不为空再添加
|
||||
if task.UserId != "" {
|
||||
tasksMap[task.UserId] = append(tasksMap[task.UserId], task)
|
||||
}
|
||||
}
|
||||
for userId, cares := range tasksMap {
|
||||
//1.查询用户
|
||||
var user system.User
|
||||
@@ -66,11 +69,11 @@ func SendCareMsg() error {
|
||||
}
|
||||
//3.构造请求参数 发送订阅消息
|
||||
payload := SendMessagePayload{
|
||||
TemplateID: "R7fh3NDpuV8DYqI83HpEQvC8mLJy5xMWFl1qeGN9JIo",
|
||||
TemplateID: "iG5GYMPQAgKxIE9zZNOgKS6tCURhM9p9AC8iZ3Uj3uA",
|
||||
Page: "pages/tasks/index",
|
||||
Touser: user.MiniOpenId,
|
||||
Data: map[string]TemplateDataItem{
|
||||
"thing2": {
|
||||
"thing1": {
|
||||
Value: myPlant.Name + "等",
|
||||
},
|
||||
"time3": {
|
||||
@@ -78,8 +81,8 @@ func SendCareMsg() error {
|
||||
Value: time.Date(now.Year(), now.Month(), now.Day(), 8, 30, 0, 0, time.Local).Format("2006-01-02"),
|
||||
},
|
||||
},
|
||||
//MiniProgramState: "formal",
|
||||
MiniProgramState: "trial",
|
||||
MiniProgramState: "formal",
|
||||
//MiniProgramState: "trial",
|
||||
Lang: "zh_CN",
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
@@ -104,10 +107,10 @@ func SendCareMsg() error {
|
||||
}
|
||||
|
||||
if smr.Errcode != 0 {
|
||||
global.Logger.Error("订阅消息发送失败!" + userId + "open-id" + user.MiniOpenId)
|
||||
return fmt.Errorf("微信服务器返回错误: errcode=%d, errmsg=%s", smr.Errcode, smr.Errmsg)
|
||||
global.Logger.Error(fmt.Sprintf("订阅消息发送失败! userId: %s, openId: %s, errcode: %d, errmsg: %s", userId, user.MiniOpenId, smr.Errcode, smr.Errmsg))
|
||||
continue // 不要因为某个用户失败(如43101没配额)而中断整个发送流程
|
||||
}
|
||||
global.Logger.Info("订阅消息发送成功!" + userId + "open-id" + user.MiniOpenId)
|
||||
global.Logger.Info(fmt.Sprintf("订阅消息发送成功! userId: %s, openId: %s", userId, user.MiniOpenId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package task
|
||||
|
||||
//
|
||||
//import (
|
||||
// "sundynix-go/global"
|
||||
// "sundynix-go/model/plant"
|
||||
// "time"
|
||||
//
|
||||
// "go.uber.org/zap"
|
||||
//)
|
||||
//
|
||||
//// GeneratorTodayCare 生成今日养护任务
|
||||
//func GeneratorTodayCare() error {
|
||||
// now := time.Now()
|
||||
// today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
//
|
||||
// //1.获取所有的养护计划
|
||||
// var plans []*plant.CarePlan
|
||||
// if err := global.DB.Find(&plans).Error; err != nil {
|
||||
// global.Logger.Error("获取所有的养护计划失败", zap.Error(err))
|
||||
// return err
|
||||
// }
|
||||
// for _, plan := range plans {
|
||||
// if plan.Period > 0 {
|
||||
// var todayCare plant.TodayCare
|
||||
// err := global.DB.Where("care_id = ?", plan.Id).First(&todayCare).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // 如果今日日期满足周期循环
|
||||
// if plan.ShouldTriggerOn() {
|
||||
// // 如果不是逾期的任务 就把ExpectedDate改为今日
|
||||
// if todayCare.Status != 4 {
|
||||
// todayCare.Status = 1
|
||||
// todayCare.ExpectedDate = today
|
||||
// if err = global.DB.Save(&todayCare).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//// UpdateExpireCare 更新过期的养护任务
|
||||
//func UpdateExpireCare() error {
|
||||
// now := time.Now()
|
||||
// // 归一化到当天的0点(本地时区)
|
||||
// today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
// var expiredCares []*plant.TodayCare
|
||||
// // 1.查询所有未完成且预计日期在今天之前的养护任务
|
||||
// if err := global.DB.Where("status = ? and expected_date < ?", 1, today).
|
||||
// Or("status = ?", 4).Find(&expiredCares).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// //2.计算过期天数并更新状态为逾期
|
||||
// for _, care := range expiredCares {
|
||||
// expireDays := int(today.Sub(care.ExpectedDate).Hours() / 24)
|
||||
// updateMap := map[string]interface{}{
|
||||
// "is_expired": 1,
|
||||
// "expire_days": expireDays,
|
||||
// "status": 4,
|
||||
// "expected_date": today,
|
||||
// }
|
||||
// if err := global.DB.Model(care).Updates(updateMap).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//// GenerateUserCenter 更新用户中心数据
|
||||
//func GenerateUserCenter() error {
|
||||
// //1.所有的用户
|
||||
// var users []plant.Personal
|
||||
// if err := global.DB.Find(&users).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// now := time.Now()
|
||||
// // 归一化到当天的0点(本地时区)
|
||||
// now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
// for _, user := range users {
|
||||
// joinDate := user.JoinDate
|
||||
// //1.加入多少天
|
||||
// joinDays := int(now.Sub(joinDate).Hours() / 24)
|
||||
// //2.植物数量
|
||||
// var plantCount int64
|
||||
// err := global.DB.Model(&plant.MyPlant{}).Where("user_id = ?", user.Id).Count(&plantCount).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// //3.养护次数
|
||||
// var careCount int64
|
||||
// err = global.DB.Model(&plant.CareRecord{}).Where("user_id = ?", user.Id).Count(&careCount).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// //4.徽章数量 todo
|
||||
// var badgeCount int64
|
||||
// err = global.DB.Model(&plant.MyBadge{}).Where("user_id = ?", user.Id).Count(&badgeCount).Error
|
||||
// user.BadgeCount = int(badgeCount)
|
||||
// // 5. 使用 Updates 方法进行精确、安全的更新
|
||||
// // 只更新我们刚刚计算和查询出的这四个字段
|
||||
// updateData := map[string]interface{}{
|
||||
// "join_days": joinDays,
|
||||
// "plant_count": plantCount,
|
||||
// "care_count": careCount,
|
||||
// "badge_count": badgeCount,
|
||||
// }
|
||||
// if err := global.DB.Model(&plant.Personal{}).Where("id = ?", user.Id).Updates(updateData).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
Reference in New Issue
Block a user