diff --git a/.gitignore b/.gitignore
index eb5b1f3e..4fdd6a86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,8 @@ cover.out
/internal/cases/internal/integration/logs
/internal/skill/internal/integration/logs
/internal/feedback/internal/integration/logs
+/internal/ai/internal/integration/logs
+/internal/resume/internal/integration/logs
/config/cert/
local_test.go
diff --git a/.run/webook.run.xml b/.run/webook.run.xml
index 62190a45..003a6c57 100644
--- a/.run/webook.run.xml
+++ b/.run/webook.run.xml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/.script/setup.sh b/.script/setup.sh
index 75bc09c5..d8953228 100644
--- a/.script/setup.sh
+++ b/.script/setup.sh
@@ -28,7 +28,7 @@ test -x $TARGET_PUSH || chmod +x $TARGET_PUSH
test -x $TARGET_COMMIT || chmod +x $TARGET_COMMIT
echo "安装 golangci-lint..."
-go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2
+go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0
echo "安装 goimports..."
go install golang.org/x/tools/cmd/goimports@latest
\ No newline at end of file
diff --git a/internal/ai/internal/domain/jd.go b/internal/ai/internal/domain/jd.go
new file mode 100644
index 00000000..b82bcf73
--- /dev/null
+++ b/internal/ai/internal/domain/jd.go
@@ -0,0 +1,19 @@
+package domain
+
+const (
+ AnalysisJDTech = "analysis_jd_tech"
+ AnalysisJDBiz = "analysis_jd_biz"
+ AnalysisJDPosition = "analysis_jd_position"
+)
+
+type JDEvaluation struct {
+ Score float64 `json:"score"`
+ Analysis string `json:"analysis"`
+}
+
+type JD struct {
+ Amount int64
+ TechScore *JDEvaluation
+ BizScore *JDEvaluation
+ PosScore *JDEvaluation
+}
diff --git a/internal/ai/internal/domain/llm.go b/internal/ai/internal/domain/llm.go
index 1f145ab6..ea55f011 100644
--- a/internal/ai/internal/domain/llm.go
+++ b/internal/ai/internal/domain/llm.go
@@ -43,6 +43,8 @@ type LLMResponse struct {
}
type BizConfig struct {
+ Id int64
+ Biz string
// 使用的模型
Model string
// 多少分钱/1000 token
diff --git a/internal/ai/internal/errs/code.go b/internal/ai/internal/errs/code.go
new file mode 100644
index 00000000..bc763b59
--- /dev/null
+++ b/internal/ai/internal/errs/code.go
@@ -0,0 +1,11 @@
+package errs
+
+var (
+ SystemError = ErrorCode{Code: 516001, Msg: "系统错误"}
+ InsufficientCredit = ErrorCode{Code: 516002, Msg: "积分不足"}
+)
+
+type ErrorCode struct {
+ Code int
+ Msg string
+}
diff --git a/internal/ai/internal/integration/llm_config_test.go b/internal/ai/internal/integration/llm_config_test.go
new file mode 100644
index 00000000..9039def2
--- /dev/null
+++ b/internal/ai/internal/integration/llm_config_test.go
@@ -0,0 +1,306 @@
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/ecodeclub/ekit/iox"
+ "github.com/ecodeclub/ginx/session"
+ "github.com/ecodeclub/webook/internal/ai/internal/integration/startup"
+ "github.com/ecodeclub/webook/internal/ai/internal/repository/dao"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
+ "github.com/ecodeclub/webook/internal/credit"
+ "github.com/ecodeclub/webook/internal/test"
+ testioc "github.com/ecodeclub/webook/internal/test/ioc"
+ "github.com/gin-gonic/gin"
+ "github.com/gotomicro/ego/core/econf"
+ "github.com/gotomicro/ego/server/egin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+)
+
+type ConfigSuite struct {
+ suite.Suite
+ db *gorm.DB
+ adminHandler *web.AdminHandler
+ server *egin.Component
+}
+
+func (s *ConfigSuite) SetupSuite() {
+ db := testioc.InitDB()
+ s.db = db
+ err := dao.InitTables(db)
+ s.NoError(err)
+ // 先插入 BizConfig
+ mou, err := startup.InitModule(s.db, nil, &credit.Module{})
+ require.NoError(s.T(), err)
+ s.adminHandler = mou.AdminHandler
+ econf.Set("server", map[string]any{"contextTimeout": "1s"})
+ server := egin.Load("server").Build()
+ server.Use(func(ctx *gin.Context) {
+ ctx.Set("_session", session.NewMemorySession(session.Claims{
+ Uid: 123,
+ }))
+ })
+ s.adminHandler.RegisterRoutes(server.Engine)
+ s.server = server
+}
+
+func (s *ConfigSuite) TestConfig_Save() {
+ testCases := []struct {
+ name string
+ config web.ConfigRequest
+ before func(t *testing.T)
+ after func(t *testing.T, id int64)
+ wantCode int
+ id int64
+ }{
+ {
+ name: "新增",
+ config: web.ConfigRequest{
+ Config: web.Config{
+ Biz: "test",
+ MaxInput: 10,
+ Model: "testModel",
+ Price: 100,
+ Temperature: 0.5,
+ TopP: 0.5,
+ SystemPrompt: "testPrompt",
+ PromptTemplate: "testTemplate",
+ KnowledgeId: "testKnowledgeId",
+ },
+ },
+ before: func(t *testing.T) {
+
+ },
+ wantCode: 200,
+ id: 1,
+ after: func(t *testing.T, id int64) {
+ var conf dao.BizConfig
+ err := s.db.WithContext(context.Background()).
+ Where("id = ?", id).First(&conf).Error
+ require.NoError(t, err)
+ s.assertBizConfig(dao.BizConfig{
+ Id: 1,
+ Biz: "test",
+ MaxInput: 10,
+ Model: "testModel",
+ Price: 100,
+ Temperature: 0.5,
+ TopP: 0.5,
+ SystemPrompt: "testPrompt",
+ PromptTemplate: "testTemplate",
+ KnowledgeId: "testKnowledgeId",
+ }, conf)
+ },
+ },
+ {
+ name: "更新",
+ config: web.ConfigRequest{
+ Config: web.Config{
+ Id: 2,
+ Biz: "2_test",
+ MaxInput: 102,
+ Model: "2_testModel",
+ Price: 102,
+ Temperature: 2.5,
+ TopP: 2.5,
+ SystemPrompt: "testPrompt2",
+ PromptTemplate: "testTemplate2",
+ KnowledgeId: "testKnowledgeId2",
+ },
+ },
+ before: func(t *testing.T) {
+ err := s.db.WithContext(context.Background()).
+ Table("ai_biz_configs").
+ Create(dao.BizConfig{
+ Id: 2,
+ Biz: "test_2",
+ MaxInput: 100,
+ Model: "testModel",
+ Price: 100,
+ Temperature: 0.5,
+ TopP: 0.5,
+ SystemPrompt: "testPrompt",
+ PromptTemplate: "testTemplate",
+ KnowledgeId: "testKnowledgeId",
+ Ctime: 11,
+ Utime: 22,
+ }).Error
+ require.NoError(t, err)
+ },
+ after: func(t *testing.T, id int64) {
+ var conf dao.BizConfig
+ err := s.db.WithContext(context.Background()).
+ Where("id = ?", id).
+ Model(&dao.BizConfig{}).
+ First(&conf).Error
+ require.NoError(t, err)
+ s.assertBizConfig(dao.BizConfig{
+ Id: 2,
+ Biz: "2_test",
+ MaxInput: 102,
+ Model: "2_testModel",
+ Price: 102,
+ Temperature: 2.5,
+ TopP: 2.5,
+ SystemPrompt: "testPrompt2",
+ PromptTemplate: "testTemplate2",
+ KnowledgeId: "testKnowledgeId2",
+ }, conf)
+ },
+ wantCode: 200,
+ id: 2,
+ },
+ }
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ req, err := http.NewRequest(http.MethodPost,
+ "/ai/config/save", iox.NewJSONReader(tc.config))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(t, err)
+ recorder := test.NewJSONResponseRecorder[int64]()
+ s.server.ServeHTTP(recorder, req)
+ require.Equal(t, tc.wantCode, recorder.Code)
+ id := recorder.MustScan().Data
+ assert.Equal(t, tc.id, id)
+ tc.after(t, id)
+ err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error
+ require.NoError(s.T(), err)
+ })
+ }
+}
+
+func (s *ConfigSuite) TestConfig_List() {
+ configs := make([]dao.BizConfig, 0, 32)
+ for i := 1; i < 10; i++ {
+ cfg := dao.BizConfig{
+ Id: int64(i),
+ Biz: fmt.Sprintf("biz_%d", i),
+ MaxInput: 100,
+ Model: fmt.Sprintf("test_model_%d", i),
+ Price: 1000,
+ Temperature: 37.5,
+ TopP: 0.8,
+ SystemPrompt: "test_prompt",
+ PromptTemplate: "test_template",
+ KnowledgeId: "test_knowledge",
+ }
+ configs = append(configs, cfg)
+ }
+ err := s.db.WithContext(context.Background()).Create(&configs).Error
+ require.NoError(s.T(), err)
+ req, err := http.NewRequest(http.MethodGet,
+ "/ai/config/list", iox.NewJSONReader(nil))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(s.T(), err)
+ recorder := test.NewJSONResponseRecorder[[]web.Config]()
+ s.server.ServeHTTP(recorder, req)
+ require.Equal(s.T(), 200, recorder.Code)
+ confs := recorder.MustScan().Data
+ assert.Equal(s.T(), getWantConfigs(), confs)
+ err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error
+ require.NoError(s.T(), err)
+}
+
+func (s *ConfigSuite) Test_Detail() {
+ testcases := []struct {
+ name string
+ req web.ConfigInfoReq
+ before func(t *testing.T)
+ wantCode int
+ wantData web.Config
+ }{
+ {
+ name: "获取配置",
+ wantCode: 200,
+ req: web.ConfigInfoReq{
+ Id: 3,
+ },
+ before: func(t *testing.T) {
+ err := s.db.WithContext(context.Background()).
+ Table("ai_biz_configs").
+ Create(dao.BizConfig{
+ Id: 3,
+ Biz: "test_3",
+ MaxInput: 100,
+ Model: "testModel",
+ Price: 100,
+ Temperature: 0.5,
+ TopP: 0.5,
+ SystemPrompt: "testPrompt",
+ PromptTemplate: "testTemplate",
+ KnowledgeId: "testKnowledgeId",
+ Ctime: 11,
+ Utime: 22,
+ }).Error
+ require.NoError(t, err)
+ },
+ wantData: web.Config{
+ Id: 3,
+ Biz: "test_3",
+ MaxInput: 100,
+ Model: "testModel",
+ Price: 100,
+ Temperature: 0.5,
+ TopP: 0.5,
+ SystemPrompt: "testPrompt",
+ PromptTemplate: "testTemplate",
+ KnowledgeId: "testKnowledgeId",
+ },
+ },
+ }
+ for _, tc := range testcases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ req, err := http.NewRequest(http.MethodPost,
+ "/ai/config/detail", iox.NewJSONReader(tc.req))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(t, err)
+ recorder := test.NewJSONResponseRecorder[web.Config]()
+ s.server.ServeHTTP(recorder, req)
+ require.Equal(s.T(), 200, recorder.Code)
+ conf := recorder.MustScan().Data
+ assert.Equal(t, tc.wantData, conf)
+ err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error
+ require.NoError(s.T(), err)
+ })
+ }
+}
+
+func getWantConfigs() []web.Config {
+ configs := make([]web.Config, 0, 32)
+ for i := 9; i >= 1; i-- {
+ cfg := web.Config{
+ Id: int64(i),
+ Biz: fmt.Sprintf("biz_%d", i),
+ MaxInput: 100,
+ Model: fmt.Sprintf("test_model_%d", i),
+ Price: 1000,
+ Temperature: 37.5,
+ TopP: 0.8,
+ SystemPrompt: "test_prompt",
+ PromptTemplate: "test_template",
+ KnowledgeId: "test_knowledge",
+ }
+ configs = append(configs, cfg)
+ }
+ return configs
+}
+
+func (s *ConfigSuite) assertBizConfig(wantConfig dao.BizConfig, actualConfig dao.BizConfig) {
+ assert.True(s.T(), actualConfig.Ctime > 0)
+ assert.True(s.T(), actualConfig.Utime > 0)
+ actualConfig.Ctime = 0
+ actualConfig.Utime = 0
+ assert.Equal(s.T(), wantConfig, actualConfig)
+}
+
+func TestConfigSuite(t *testing.T) {
+ suite.Run(t, new(ConfigSuite))
+}
diff --git a/internal/ai/internal/integration/llm_service_test.go b/internal/ai/internal/integration/llm_service_test.go
index 74a4539c..d285f574 100644
--- a/internal/ai/internal/integration/llm_service_test.go
+++ b/internal/ai/internal/integration/llm_service_test.go
@@ -5,9 +5,18 @@ package integration
import (
"context"
"errors"
+ "net/http"
"testing"
"time"
+ "github.com/ecodeclub/ekit/iox"
+ "github.com/ecodeclub/ginx/session"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
+ "github.com/ecodeclub/webook/internal/test"
+ "github.com/gin-gonic/gin"
+ "github.com/gotomicro/ego/core/econf"
+ "github.com/gotomicro/ego/server/egin"
+
"github.com/ecodeclub/ekit/sqlx"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm"
hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks"
@@ -65,6 +74,37 @@ func (s *LLMServiceSuite) SetupSuite() {
Utime: now,
}).Error
s.NoError(err)
+
+ err = s.db.Create(&dao.BizConfig{
+ Biz: domain.AnalysisJDBiz,
+ MaxInput: 100,
+ PromptTemplate: "这是岗位描述 %s",
+ KnowledgeId: knowledgeId,
+ Ctime: now,
+ Utime: now,
+ }).Error
+ s.NoError(err)
+
+ err = s.db.Create(&dao.BizConfig{
+ Biz: domain.AnalysisJDTech,
+ MaxInput: 100,
+ PromptTemplate: "这是岗位描述tech %s",
+ KnowledgeId: knowledgeId,
+ Ctime: now,
+ Utime: now,
+ }).Error
+ s.NoError(err)
+
+ err = s.db.Create(&dao.BizConfig{
+ Biz: domain.AnalysisJDPosition,
+ MaxInput: 100,
+ PromptTemplate: "这是岗位描述position %s",
+ KnowledgeId: knowledgeId,
+ Ctime: now,
+ Utime: now,
+ }).Error
+ s.NoError(err)
+
}
func (s *LLMServiceSuite) TearDownSuite() {
@@ -461,6 +501,233 @@ func (s *LLMServiceSuite) TestService() {
}
}
+func (s *LLMServiceSuite) TestHandler_Ask() {
+ testCases := []struct {
+ name string
+ req web.LLMRequest
+ before func(t *testing.T, ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service)
+ assertFunc assert.ErrorAssertionFunc
+ after func(t *testing.T, resp web.LLMResponse)
+ wantCode int
+ }{
+ {
+ name: "八股文web-成功",
+ req: web.LLMRequest{
+ Biz: domain.BizQuestionExamine,
+ Input: []string{
+ "问题1",
+ "问题1内容",
+ "用户输入1",
+ },
+ },
+ assertFunc: assert.NoError,
+ before: func(t *testing.T,
+ ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) {
+ llmHdl := hdlmocks.NewMockHandler(ctrl)
+ llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()).
+ Return(domain.LLMResponse{
+ Tokens: 100,
+ Amount: 100,
+ Answer: "aians",
+ }, nil)
+ creditSvc := creditmocks.NewMockService(ctrl)
+ creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{
+ TotalAmount: 1000,
+ }, nil)
+ creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).Return(11, nil)
+ creditSvc.EXPECT().ConfirmDeductCredits(gomock.Any(), int64(123), int64(11)).Return(nil)
+ return llmHdl, creditSvc
+ },
+ after: func(t *testing.T, resp web.LLMResponse) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 校验response写入的内容是否正确
+ assert.Equal(t, web.LLMResponse{
+ Amount: 100,
+ RawResult: "aians",
+ }, resp)
+
+ var logModel dao.LLMRecord
+ err := s.db.WithContext(ctx).Where("id = ?", 1).First(&logModel).Error
+ require.NoError(t, err)
+ assert.True(t, logModel.Tid != "")
+ s.assertLog(dao.LLMRecord{
+ Id: 1,
+ Uid: 123,
+ Tid: logModel.Tid,
+ Biz: domain.BizQuestionExamine,
+ Tokens: 100,
+ Amount: 100,
+ KnowledgeId: knowledgeId,
+ Input: sqlx.JsonColumn[[]string]{
+ Valid: true,
+ Val: []string{
+ "问题1",
+ "问题1内容",
+ "用户输入1",
+ },
+ },
+ Status: 1,
+ PromptTemplate: sqlx.NewNullString("这是问题 %s,这是问题内容 %s,这是用户输入 %s"),
+ Answer: sqlx.NewNullString("aians"),
+ }, logModel)
+ // 校验credit写入的内容是否正确
+ var creditLogModel dao.LLMCredit
+ err = s.db.WithContext(ctx).Where("id = ?", 1).First(&creditLogModel).Error
+ require.NoError(t, err)
+ assert.True(t, logModel.Tid != "")
+
+ s.assertCreditLog(dao.LLMCredit{
+ Id: 1,
+ Tid: logModel.Tid,
+ Uid: 123,
+ Biz: domain.BizQuestionExamine,
+ Amount: 100,
+ Status: 1,
+ }, creditLogModel)
+ },
+ wantCode: 200,
+ },
+ }
+ for _, tc := range testCases {
+ tc := tc
+ s.T().Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ mockHdl, mockCredit := tc.before(t, ctrl)
+ mou, err := startup.InitModule(s.db, mockHdl, &credit.Module{Svc: mockCredit})
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/ai/ask", iox.NewJSONReader(tc.req))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(t, err)
+ econf.Set("server", map[string]any{"contextTimeout": "1s"})
+
+ server := egin.Load("server").Build()
+ server.Use(func(ctx *gin.Context) {
+ ctx.Set(session.CtxSessionKey,
+ session.NewMemorySession(session.Claims{
+ Uid: 123,
+ }))
+ })
+ mou.Hdl.MemberRoutes(server.Engine)
+ recorder := test.NewJSONResponseRecorder[web.LLMResponse]()
+ server.ServeHTTP(recorder, req)
+ require.Equal(t, tc.wantCode, recorder.Code)
+ tc.after(t, recorder.MustScan().Data)
+ })
+ }
+}
+
+func (s *LLMServiceSuite) TestHandler_AnalysisJD() {
+ testCases := []struct {
+ name string
+ req web.JDRequest
+ before func(t *testing.T, ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service)
+ assertFunc assert.ErrorAssertionFunc
+ after func(t *testing.T, resp web.JDResponse)
+ wantCode int
+ }{
+ {
+ name: "岗位测评",
+ req: web.JDRequest{
+ JD: "我们招聘一个go工程师",
+ },
+ assertFunc: assert.NoError,
+ before: func(t *testing.T,
+ ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) {
+ llmHdl := hdlmocks.NewMockHandler(ctrl)
+ llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, request domain.LLMRequest) (domain.LLMResponse, error) {
+ if request.Biz == "analysis_jd_tech" {
+ return domain.LLMResponse{
+ Tokens: 1000,
+ Amount: 100,
+ Answer: `score: 6
+这是技术前景`,
+ }, nil
+ }
+ if request.Biz == "analysis_jd_biz" {
+ return domain.LLMResponse{
+ Tokens: 100,
+ Amount: 200,
+ Answer: `score: 7
+这是业务前景`,
+ }, nil
+ }
+ if request.Biz == "analysis_jd_position" {
+ return domain.LLMResponse{
+ Tokens: 100,
+ Amount: 100,
+ Answer: `score: 8
+这是公司地位`,
+ }, nil
+ }
+ return domain.LLMResponse{}, errors.New("unknown biz")
+ }).AnyTimes()
+ creditSvc := creditmocks.NewMockService(ctrl)
+ creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{
+ TotalAmount: 200000,
+ }, nil).AnyTimes()
+ creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).Return(11, nil).AnyTimes()
+ creditSvc.EXPECT().ConfirmDeductCredits(gomock.Any(), int64(123), int64(11)).Return(nil).AnyTimes()
+ return llmHdl, creditSvc
+ },
+ after: func(t *testing.T, resp web.JDResponse) {
+ // 校验response写入的内容是否正确
+ assert.Equal(t, web.JDResponse{
+ Amount: 400,
+ TechScore: &web.JDEvaluation{
+ Score: 6,
+ Analysis: "这是技术前景",
+ },
+ BizScore: &web.JDEvaluation{
+ Score: 7,
+ Analysis: "这是业务前景",
+ },
+ PosScore: &web.JDEvaluation{
+ Score: 8,
+ Analysis: "这是公司地位",
+ },
+ }, resp)
+
+ },
+ wantCode: 200,
+ },
+ }
+ for _, tc := range testCases {
+ tc := tc
+ s.T().Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ mockHdl, mockCredit := tc.before(t, ctrl)
+ mou, err := startup.InitModule(s.db, mockHdl, &credit.Module{Svc: mockCredit})
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/ai/analysis_jd", iox.NewJSONReader(tc.req))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(t, err)
+ econf.Set("server", map[string]any{"contextTimeout": "1s"})
+ server := egin.Load("server").Build()
+ server.Use(func(ctx *gin.Context) {
+ ctx.Set(session.CtxSessionKey,
+ session.NewMemorySession(session.Claims{
+ Uid: 123,
+ }))
+ })
+ mou.Hdl.MemberRoutes(server.Engine)
+ recorder := test.NewJSONResponseRecorder[web.JDResponse]()
+ server.ServeHTTP(recorder, req)
+ require.Equal(t, tc.wantCode, recorder.Code)
+ tc.after(t, recorder.MustScan().Data)
+ err = s.db.Exec("TRUNCATE TABLE `llm_records`").Error
+ require.NoError(s.T(), err)
+ err = s.db.Exec("TRUNCATE TABLE `llm_credits`").Error
+ require.NoError(s.T(), err)
+ })
+ }
+}
+
func (s *LLMServiceSuite) assertLog(wantLog dao.LLMRecord, actual dao.LLMRecord) {
require.True(s.T(), actual.Ctime != 0)
require.True(s.T(), actual.Utime != 0)
diff --git a/internal/ai/internal/integration/startup/wire.go b/internal/ai/internal/integration/startup/wire.go
index 1146c0e3..49349210 100644
--- a/internal/ai/internal/integration/startup/wire.go
+++ b/internal/ai/internal/integration/startup/wire.go
@@ -5,6 +5,9 @@ package startup
import (
"sync"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
+
hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks"
"github.com/ecodeclub/webook/internal/ai"
@@ -43,7 +46,11 @@ func InitModule(db *egorm.Component,
ai.InitCommonHandlers,
InitRootHandler,
-
+ service.NewGeneralService,
+ service.NewJDService,
+ service.NewConfigService,
+ web.NewHandler,
+ web.NewAdminHandler,
wire.Struct(new(ai.Module), "*"),
wire.FieldsOf(new(*credit.Module), "Svc"),
)
diff --git a/internal/ai/internal/integration/startup/wire_gen.go b/internal/ai/internal/integration/startup/wire_gen.go
index d9129c10..8adcb6ec 100644
--- a/internal/ai/internal/integration/startup/wire_gen.go
+++ b/internal/ai/internal/integration/startup/wire_gen.go
@@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
-//go:generate go run github.com/google/wire/cmd/wire
+//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
@@ -12,6 +12,7 @@ import (
"github.com/ecodeclub/webook/internal/ai"
"github.com/ecodeclub/webook/internal/ai/internal/repository"
"github.com/ecodeclub/webook/internal/ai/internal/repository/dao"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config"
@@ -19,6 +20,7 @@ import (
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log"
hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
"github.com/ecodeclub/webook/internal/credit"
"github.com/ego-component/egorm"
"gorm.io/gorm"
@@ -31,18 +33,25 @@ func InitModule(db *gorm.DB, hdl *hdlmocks.MockHandler, creditSvc *credit.Module
configDAO := dao.NewGORMConfigDAO(db)
configRepository := repository.NewCachedConfigRepository(configDAO)
configHandlerBuilder := config.NewBuilder(configRepository)
- service := creditSvc.Svc
+ serviceService := creditSvc.Svc
llmCreditDAO := InitLLMCreditLogDAO(db)
llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO)
- creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo)
+ creditHandlerBuilder := credit2.NewHandlerBuilder(serviceService, llmCreditLogRepo)
llmRecordDAO := dao.NewGORMLLMLogDAO(db)
llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO)
recordHandlerBuilder := record.NewHandler(llmLogRepo)
v := ai.InitCommonHandlers(handlerBuilder, configHandlerBuilder, creditHandlerBuilder, recordHandlerBuilder)
handler := InitRootHandler(v, hdl)
llmService := llm.NewLLMService(handler)
+ generalService := service.NewGeneralService(llmService)
+ jdService := service.NewJDService(llmService)
+ webHandler := web.NewHandler(generalService, jdService)
+ configService := service.NewConfigService(configRepository)
+ adminHandler := web.NewAdminHandler(configService)
module := &ai.Module{
- Svc: llmService,
+ Svc: llmService,
+ Hdl: webHandler,
+ AdminHandler: adminHandler,
}
return module, nil
}
diff --git a/internal/ai/internal/repository/config.go b/internal/ai/internal/repository/config.go
index f8ddcd01..526a2f4d 100644
--- a/internal/ai/internal/repository/config.go
+++ b/internal/ai/internal/repository/config.go
@@ -17,12 +17,16 @@ package repository
import (
"context"
+ "github.com/ecodeclub/ekit/slice"
"github.com/ecodeclub/webook/internal/ai/internal/domain"
"github.com/ecodeclub/webook/internal/ai/internal/repository/dao"
)
type ConfigRepository interface {
GetConfig(ctx context.Context, biz string) (domain.BizConfig, error)
+ Save(ctx context.Context, cfg domain.BizConfig) (int64, error)
+ List(ctx context.Context) ([]domain.BizConfig, error)
+ GetById(ctx context.Context, id int64) (domain.BizConfig, error)
}
// CachedConfigRepository 这个是一定要搞缓存的
@@ -35,6 +39,61 @@ func NewCachedConfigRepository(dao dao.ConfigDAO) ConfigRepository {
return &CachedConfigRepository{dao: dao}
}
+// Save 保存配置
+func (r *CachedConfigRepository) Save(ctx context.Context, cfg domain.BizConfig) (int64, error) {
+ return r.dao.Save(ctx, dao.BizConfig{
+ Id: cfg.Id,
+ Biz: cfg.Biz,
+ MaxInput: cfg.MaxInput,
+ Model: cfg.Model,
+ Price: cfg.Price,
+ Temperature: cfg.Temperature,
+ TopP: cfg.TopP,
+ SystemPrompt: cfg.SystemPrompt,
+ PromptTemplate: cfg.PromptTemplate,
+ KnowledgeId: cfg.KnowledgeId,
+ })
+}
+func (r *CachedConfigRepository) List(ctx context.Context) ([]domain.BizConfig, error) {
+ configs, err := r.dao.List(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(configs, func(idx int, src dao.BizConfig) domain.BizConfig {
+ return domain.BizConfig{
+ Id: src.Id,
+ Biz: src.Biz,
+ Model: src.Model,
+ Price: src.Price,
+ Temperature: src.Temperature,
+ TopP: src.TopP,
+ SystemPrompt: src.SystemPrompt,
+ MaxInput: src.MaxInput,
+ KnowledgeId: src.KnowledgeId,
+ PromptTemplate: src.PromptTemplate,
+ }
+ }), nil
+}
+
+func (r *CachedConfigRepository) GetById(ctx context.Context, id int64) (domain.BizConfig, error) {
+ cfg, err := r.dao.GetById(ctx, id)
+ if err != nil {
+ return domain.BizConfig{}, err
+ }
+
+ return domain.BizConfig{
+ Id: cfg.Id,
+ Biz: cfg.Biz,
+ Model: cfg.Model,
+ Price: cfg.Price,
+ Temperature: cfg.Temperature,
+ TopP: cfg.TopP,
+ SystemPrompt: cfg.SystemPrompt,
+ MaxInput: cfg.MaxInput,
+ KnowledgeId: cfg.KnowledgeId,
+ PromptTemplate: cfg.PromptTemplate,
+ }, nil
+}
func (repo *CachedConfigRepository) GetConfig(ctx context.Context, biz string) (domain.BizConfig, error) {
res, err := repo.dao.GetConfig(ctx, biz)
if err != nil {
diff --git a/internal/ai/internal/repository/dao/config.go b/internal/ai/internal/repository/dao/config.go
index c7d88b53..6bd858ee 100644
--- a/internal/ai/internal/repository/dao/config.go
+++ b/internal/ai/internal/repository/dao/config.go
@@ -16,12 +16,17 @@ package dao
import (
"context"
+ "time"
"github.com/ego-component/egorm"
+ "gorm.io/gorm/clause"
)
type ConfigDAO interface {
GetConfig(ctx context.Context, biz string) (BizConfig, error)
+ Save(ctx context.Context, cfg BizConfig) (int64, error)
+ List(ctx context.Context) ([]BizConfig, error)
+ GetById(ctx context.Context, id int64) (BizConfig, error)
}
type GORMConfigDAO struct {
@@ -32,11 +37,35 @@ func NewGORMConfigDAO(db *egorm.Component) ConfigDAO {
return &GORMConfigDAO{db: db}
}
+func (dao *GORMConfigDAO) GetById(ctx context.Context, id int64) (BizConfig, error) {
+ var config BizConfig
+ err := dao.db.WithContext(ctx).Where("id = ?", id).First(&config).Error
+ return config, err
+}
+
+func (dao *GORMConfigDAO) List(ctx context.Context) ([]BizConfig, error) {
+ var configs []BizConfig
+ err := dao.db.WithContext(ctx).
+ Model(&BizConfig{}).
+ Order("id desc").
+ Find(&configs).Error
+ return configs, err
+}
func (dao *GORMConfigDAO) GetConfig(ctx context.Context, biz string) (BizConfig, error) {
var res BizConfig
err := dao.db.WithContext(ctx).Where("biz = ?", biz).First(&res).Error
return res, err
}
+func (dao *GORMConfigDAO) Save(ctx context.Context, cfg BizConfig) (int64, error) {
+ now := time.Now().UnixMilli()
+ cfg.Utime = now
+ cfg.Ctime = now
+ err := dao.db.WithContext(ctx).Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"biz", "max_input", "model", "price", "temperature", "top_p", "system_prompt", "prompt_template", "knowledge_id", "utime"}),
+ }).Create(&cfg).Error
+ return cfg.Id, err
+}
type BizConfig struct {
Id int64 `gorm:"primaryKey;autoIncrement;comment:AI biz 配置表ID"`
diff --git a/internal/ai/internal/service/config_service.go b/internal/ai/internal/service/config_service.go
new file mode 100644
index 00000000..4c1863c0
--- /dev/null
+++ b/internal/ai/internal/service/config_service.go
@@ -0,0 +1,46 @@
+package service
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/ecodeclub/webook/internal/ai/internal/domain"
+ "github.com/ecodeclub/webook/internal/ai/internal/repository"
+)
+
+// ConfigService 定义配置服务的接口
+type ConfigService interface {
+ Save(ctx context.Context, cfg domain.BizConfig) (int64, error)
+ List(ctx context.Context) ([]domain.BizConfig, error)
+ GetById(ctx context.Context, id int64) (domain.BizConfig, error)
+}
+
+// configService 具体实现
+type configService struct {
+ repo repository.ConfigRepository
+}
+
+// NewConfigService 创建 ConfigService 实例
+func NewConfigService(repo repository.ConfigRepository) ConfigService {
+ return &configService{
+ repo: repo,
+ }
+}
+
+// Save 保存配置
+func (s *configService) Save(ctx context.Context, cfg domain.BizConfig) (int64, error) {
+ return s.repo.Save(ctx, cfg)
+}
+
+// List 获取所有配置列表
+func (s *configService) List(ctx context.Context) ([]domain.BizConfig, error) {
+ return s.repo.List(ctx)
+}
+
+// GetById 根据ID获取配置
+func (s *configService) GetById(ctx context.Context, id int64) (domain.BizConfig, error) {
+ if id <= 0 {
+ return domain.BizConfig{}, fmt.Errorf("无效的ID")
+ }
+ return s.repo.GetById(ctx, id)
+}
diff --git a/internal/ai/internal/service/general_service.go b/internal/ai/internal/service/general_service.go
new file mode 100644
index 00000000..49ad00e1
--- /dev/null
+++ b/internal/ai/internal/service/general_service.go
@@ -0,0 +1,35 @@
+package service
+
+import (
+ "context"
+
+ "github.com/ecodeclub/webook/internal/ai/internal/domain"
+ "github.com/ecodeclub/webook/internal/ai/internal/service/llm"
+ "github.com/lithammer/shortuuid/v4"
+)
+
+type GeneralService interface {
+ // LLMAsk 通用询问ai的接口
+ LLMAsk(ctx context.Context, uid int64, biz string, input []string) (domain.LLMResponse, error)
+}
+
+func NewGeneralService(aiSvc llm.Service) GeneralService {
+ return &generalSvc{
+ aiSvc: aiSvc,
+ }
+}
+
+type generalSvc struct {
+ aiSvc llm.Service
+}
+
+func (g *generalSvc) LLMAsk(ctx context.Context, uid int64, biz string, input []string) (domain.LLMResponse, error) {
+ tid := shortuuid.New()
+ aiReq := domain.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: biz,
+ Input: input,
+ }
+ return g.aiSvc.Invoke(ctx, aiReq)
+}
diff --git a/internal/ai/internal/service/jd_service.go b/internal/ai/internal/service/jd_service.go
new file mode 100644
index 00000000..093e947b
--- /dev/null
+++ b/internal/ai/internal/service/jd_service.go
@@ -0,0 +1,102 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+ "sync/atomic"
+
+ "github.com/ecodeclub/webook/internal/ai/internal/domain"
+ "github.com/ecodeclub/webook/internal/ai/internal/service/llm"
+ "github.com/lithammer/shortuuid/v4"
+ "golang.org/x/sync/errgroup"
+)
+
+type JDService interface {
+ // Evaluate 测评
+ Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error)
+}
+
+type jdSvc struct {
+ aiSvc llm.Service
+}
+
+func NewJDService(aiSvc llm.Service) JDService {
+ return &jdSvc{
+ aiSvc: aiSvc,
+ }
+}
+
+func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) {
+ var techJD, bizJD, positionJD *domain.JDEvaluation
+ var amount int64
+ var eg errgroup.Group
+ eg.Go(func() error {
+ var err error
+ var techAmount int64
+ techAmount, techJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDTech, jd)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, techAmount)
+ return nil
+ })
+ eg.Go(func() error {
+ var err error
+ var bizAmount int64
+ bizAmount, bizJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDBiz, jd)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, bizAmount)
+ return nil
+ })
+ eg.Go(func() error {
+ var err error
+ var positionAmount int64
+ positionAmount, positionJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDPosition, jd)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, positionAmount)
+ return nil
+ })
+ if err := eg.Wait(); err != nil {
+ return domain.JD{}, err
+ }
+ return domain.JD{
+ Amount: amount,
+ TechScore: techJD,
+ BizScore: bizJD,
+ PosScore: positionJD,
+ }, nil
+}
+
+func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string) (int64, *domain.JDEvaluation, error) {
+ tid := shortuuid.New()
+ aiReq := domain.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: biz,
+ Input: []string{jd},
+ }
+ resp, err := j.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, nil, err
+ }
+ answer := strings.SplitN(resp.Answer, "\n", 2)
+ if len(answer) != 2 {
+ return 0, nil, errors.New("不符合预期的大模型响应")
+ }
+ score := answer[0]
+ scoreNum, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(score, "score:")), 64)
+ if err != nil {
+ return 0, nil, errors.New("分数返回的数据不对")
+ }
+
+ return resp.Amount, &domain.JDEvaluation{
+ Score: scoreNum,
+ Analysis: strings.TrimSpace(strings.TrimPrefix(answer[1], "analysis:")),
+ }, nil
+}
diff --git a/internal/ai/internal/web/admin_handler.go b/internal/ai/internal/web/admin_handler.go
new file mode 100644
index 00000000..70a130d7
--- /dev/null
+++ b/internal/ai/internal/web/admin_handler.go
@@ -0,0 +1,89 @@
+package web
+
+import (
+ "github.com/ecodeclub/ekit/slice"
+ "github.com/ecodeclub/ginx"
+ "github.com/ecodeclub/webook/internal/ai/internal/domain"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
+ "github.com/gin-gonic/gin"
+)
+
+type AdminHandler struct {
+ svc service.ConfigService
+}
+
+func NewAdminHandler(svc service.ConfigService) *AdminHandler {
+ return &AdminHandler{
+ svc: svc,
+ }
+}
+
+func (h *AdminHandler) RegisterRoutes(server *gin.Engine) {
+ // 管理员路由组
+ admin := server.Group("/ai/config")
+ admin.POST("/save", ginx.B[ConfigRequest](h.Save))
+ admin.GET("/list", ginx.W(h.List))
+ admin.POST("/detail", ginx.B[ConfigInfoReq](h.GetById))
+}
+
+func (h *AdminHandler) Save(ctx *ginx.Context, req ConfigRequest) (ginx.Result, error) {
+ id, err := h.svc.Save(ctx, domain.BizConfig{
+ Id: req.Config.Id,
+ Biz: req.Config.Biz,
+ MaxInput: req.Config.MaxInput,
+ Model: req.Config.Model,
+ Price: req.Config.Price,
+ Temperature: req.Config.Temperature,
+ TopP: req.Config.TopP,
+ SystemPrompt: req.Config.SystemPrompt,
+ PromptTemplate: req.Config.PromptTemplate,
+ KnowledgeId: req.Config.KnowledgeId,
+ })
+ if err != nil {
+ return systemErrorResult, err
+ }
+ return ginx.Result{
+ Data: id,
+ }, nil
+}
+
+// List 获取配置列表
+func (h *AdminHandler) List(ctx *ginx.Context) (ginx.Result, error) {
+ configs, err := h.svc.List(ctx)
+ if err != nil {
+ return systemErrorResult, err
+ }
+
+ return ginx.Result{
+ Data: slice.Map(configs, func(idx int, c domain.BizConfig) Config {
+ return h.domainToConfig(c)
+ }),
+ }, nil
+}
+
+func (h *AdminHandler) GetById(ctx *ginx.Context, req ConfigInfoReq) (ginx.Result, error) {
+ id := req.Id
+ config, err := h.svc.GetById(ctx, id)
+ if err != nil {
+ return systemErrorResult, err
+ }
+
+ return ginx.Result{
+ Data: h.domainToConfig(config),
+ }, nil
+}
+
+func (h *AdminHandler) domainToConfig(cfg domain.BizConfig) Config {
+ return Config{
+ Id: cfg.Id,
+ Biz: cfg.Biz,
+ MaxInput: cfg.MaxInput,
+ Model: cfg.Model,
+ Price: cfg.Price,
+ Temperature: cfg.Temperature,
+ TopP: cfg.TopP,
+ SystemPrompt: cfg.SystemPrompt,
+ PromptTemplate: cfg.PromptTemplate,
+ KnowledgeId: cfg.KnowledgeId,
+ }
+}
diff --git a/internal/ai/internal/web/handler.go b/internal/ai/internal/web/handler.go
new file mode 100644
index 00000000..40d0d403
--- /dev/null
+++ b/internal/ai/internal/web/handler.go
@@ -0,0 +1,81 @@
+package web
+
+import (
+ "github.com/ecodeclub/ginx"
+ "github.com/ecodeclub/ginx/session"
+ "github.com/ecodeclub/webook/internal/ai/internal/domain"
+ "github.com/ecodeclub/webook/internal/ai/internal/errs"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
+ "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit"
+ "github.com/gin-gonic/gin"
+ "github.com/pkg/errors"
+)
+
+type Handler struct {
+ generalSvc service.GeneralService
+ jdSvc service.JDService
+}
+
+func NewHandler(generalSvc service.GeneralService, jdSvc service.JDService) *Handler {
+ return &Handler{
+ generalSvc: generalSvc,
+ jdSvc: jdSvc,
+ }
+}
+
+func (h *Handler) MemberRoutes(server *gin.Engine) {
+ server.POST("/ai/ask", ginx.BS(h.LLMAsk))
+ server.POST("/ai/analysis_jd", ginx.BS(h.AnalysisJd))
+}
+
+func (h *Handler) LLMAsk(ctx *ginx.Context, req LLMRequest, sess session.Session) (ginx.Result, error) {
+ uid := sess.Claims().Uid
+ resp, err := h.generalSvc.LLMAsk(ctx, uid, req.Biz, req.Input)
+ switch {
+ case errors.Is(err, credit.ErrInsufficientCredit):
+ return ginx.Result{
+ Code: errs.InsufficientCredit.Code,
+ Msg: errs.InsufficientCredit.Msg,
+ }, nil
+ case err == nil:
+ return ginx.Result{
+ Data: LLMResponse{
+ Amount: resp.Amount,
+ RawResult: resp.Answer,
+ },
+ }, nil
+ default:
+ return systemErrorResult, err
+ }
+}
+
+func (h *Handler) AnalysisJd(ctx *ginx.Context, req JDRequest, sess session.Session) (ginx.Result, error) {
+ uid := sess.Claims().Uid
+ resp, err := h.jdSvc.Evaluate(ctx, uid, req.JD)
+ switch {
+ case errors.Is(err, credit.ErrInsufficientCredit):
+ return ginx.Result{
+ Code: errs.InsufficientCredit.Code,
+ Msg: errs.InsufficientCredit.Msg,
+ }, nil
+ case err == nil:
+ return ginx.Result{
+ Data: JDResponse{
+ Amount: resp.Amount,
+ TechScore: h.newJD(resp.TechScore),
+ BizScore: h.newJD(resp.BizScore),
+ PosScore: h.newJD(resp.PosScore),
+ },
+ }, nil
+ default:
+ return systemErrorResult, err
+ }
+
+}
+
+func (h *Handler) newJD(jd *domain.JDEvaluation) *JDEvaluation {
+ return &JDEvaluation{
+ Score: jd.Score,
+ Analysis: jd.Analysis,
+ }
+}
diff --git a/internal/ai/internal/web/result.go b/internal/ai/internal/web/result.go
new file mode 100644
index 00000000..055c36f9
--- /dev/null
+++ b/internal/ai/internal/web/result.go
@@ -0,0 +1,13 @@
+package web
+
+import (
+ "github.com/ecodeclub/ginx"
+ "github.com/ecodeclub/webook/internal/ai/internal/errs"
+)
+
+var (
+ systemErrorResult = ginx.Result{
+ Code: errs.SystemError.Code,
+ Msg: errs.SystemError.Msg,
+ }
+)
diff --git a/internal/ai/internal/web/vo.go b/internal/ai/internal/web/vo.go
new file mode 100644
index 00000000..4735c6b5
--- /dev/null
+++ b/internal/ai/internal/web/vo.go
@@ -0,0 +1,46 @@
+package web
+
+type LLMRequest struct {
+ Biz string `json:"biz"`
+ Input []string `json:"input"`
+}
+
+type LLMResponse struct {
+ Amount int64 `json:"amount"`
+ RawResult string `json:"rawResult"`
+}
+
+type JDRequest struct {
+ JD string `json:"jd"`
+}
+
+type JDResponse struct {
+ Amount int64 `json:"amount"`
+ TechScore *JDEvaluation `json:"techScore"`
+ BizScore *JDEvaluation `json:"bizScore"`
+ PosScore *JDEvaluation `json:"posScore"`
+}
+
+type JDEvaluation struct {
+ Score float64 `json:"score"`
+ Analysis string `json:"analysis"`
+}
+
+type Config struct {
+ Id int64 `json:"id"`
+ Biz string `json:"biz"`
+ MaxInput int `json:"maxInput"`
+ Model string `json:"model"`
+ Price int64 `json:"price"`
+ Temperature float64 `json:"temperature"`
+ TopP float64 `json:"topP"`
+ SystemPrompt string `json:"systemPrompt"`
+ PromptTemplate string `json:"promptTemplate"`
+ KnowledgeId string `json:"knowledgeId"`
+}
+type ConfigRequest struct {
+ Config Config `json:"config"`
+}
+type ConfigInfoReq struct {
+ Id int64 `json:"id"`
+}
diff --git a/internal/ai/module.go b/internal/ai/module.go
index 895889f9..941289c7 100644
--- a/internal/ai/module.go
+++ b/internal/ai/module.go
@@ -1,5 +1,7 @@
package ai
type Module struct {
- Svc LLMService
+ Svc LLMService
+ Hdl *LLMHandler
+ AdminHandler *AdminHandler
}
diff --git a/internal/ai/type.go b/internal/ai/type.go
index 6ef8be1c..66a813b9 100644
--- a/internal/ai/type.go
+++ b/internal/ai/type.go
@@ -3,8 +3,11 @@ package ai
import (
"github.com/ecodeclub/webook/internal/ai/internal/domain"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
)
type LLMRequest = domain.LLMRequest
type LLMResponse = domain.LLMResponse
type LLMService = llm.Service
+type AdminHandler = web.AdminHandler
+type LLMHandler = web.Handler
diff --git a/internal/ai/wire.go b/internal/ai/wire.go
index 5304bd54..38640e9f 100644
--- a/internal/ai/wire.go
+++ b/internal/ai/wire.go
@@ -5,6 +5,9 @@ package ai
import (
"sync"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
+
"github.com/ecodeclub/webook/internal/ai/internal/service/llm"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config"
aicredit "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit"
@@ -38,7 +41,11 @@ func InitModule(db *egorm.Component, creditSvc *credit.Module) (*Module, error)
InitCompositionHandlerUsingZhipu,
InitCommonHandlers,
InitZhipu,
-
+ service.NewGeneralService,
+ service.NewJDService,
+ service.NewConfigService,
+ web.NewHandler,
+ web.NewAdminHandler,
wire.Struct(new(Module), "*"),
wire.FieldsOf(new(*credit.Module), "Svc"),
)
diff --git a/internal/ai/wire_gen.go b/internal/ai/wire_gen.go
index aba23935..360f42ef 100644
--- a/internal/ai/wire_gen.go
+++ b/internal/ai/wire_gen.go
@@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
-//go:generate go run github.com/google/wire/cmd/wire
+//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
@@ -11,11 +11,13 @@ import (
"github.com/ecodeclub/webook/internal/ai/internal/repository"
"github.com/ecodeclub/webook/internal/ai/internal/repository/dao"
+ "github.com/ecodeclub/webook/internal/ai/internal/service"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config"
credit2 "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log"
"github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record"
+ "github.com/ecodeclub/webook/internal/ai/internal/web"
"github.com/ecodeclub/webook/internal/credit"
"github.com/ego-component/egorm"
"gorm.io/gorm"
@@ -28,10 +30,10 @@ func InitModule(db *gorm.DB, creditSvc *credit.Module) (*Module, error) {
configDAO := dao.NewGORMConfigDAO(db)
configRepository := repository.NewCachedConfigRepository(configDAO)
configHandlerBuilder := config.NewBuilder(configRepository)
- service := creditSvc.Svc
+ serviceService := creditSvc.Svc
llmCreditDAO := InitLLMCreditLogDAO(db)
llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO)
- creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo)
+ creditHandlerBuilder := credit2.NewHandlerBuilder(serviceService, llmCreditLogRepo)
llmRecordDAO := dao.NewGORMLLMLogDAO(db)
llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO)
recordHandlerBuilder := record.NewHandler(llmLogRepo)
@@ -39,8 +41,15 @@ func InitModule(db *gorm.DB, creditSvc *credit.Module) (*Module, error) {
handler := InitZhipu()
handlerHandler := InitCompositionHandlerUsingZhipu(v, handler)
llmService := llm.NewLLMService(handlerHandler)
+ generalService := service.NewGeneralService(llmService)
+ jdService := service.NewJDService(llmService)
+ webHandler := web.NewHandler(generalService, jdService)
+ configService := service.NewConfigService(configRepository)
+ adminHandler := web.NewAdminHandler(configService)
module := &Module{
- Svc: llmService,
+ Svc: llmService,
+ Hdl: webHandler,
+ AdminHandler: adminHandler,
}
return module, nil
}
diff --git a/internal/resume/internal/domain/analysis.go b/internal/resume/internal/domain/analysis.go
new file mode 100644
index 00000000..7de71b4f
--- /dev/null
+++ b/internal/resume/internal/domain/analysis.go
@@ -0,0 +1,21 @@
+package domain
+
+const (
+ BizResumeSkillKeyPoints = "biz_resume_skill_keypoints"
+ BizSkillsRewrite = "biz_resume_skill_rewrite"
+ // BizResumeProjectEvaluation 评价项目经历
+ BizResumeProjectEvaluation = "biz_resume_project_evaluation"
+ BizResumeProjectRewrite = "biz_resume_project_rewrite"
+ //BizResumeJobsKeyPoints = "biz_resume_job_keypoints"
+ BizResumeJobsRewrite = "biz_resume_job_rewrite"
+)
+
+type ResumeAnalysis struct {
+ Amount int64
+ // 技能
+ RewriteSkills string
+ // 项目
+ RewriteProject string
+ // 工作经历
+ RewriteJobs string
+}
diff --git a/internal/resume/internal/errs/code.go b/internal/resume/internal/errs/code.go
index 5a7b4438..e2ba1e77 100644
--- a/internal/resume/internal/errs/code.go
+++ b/internal/resume/internal/errs/code.go
@@ -2,6 +2,8 @@ package errs
var (
SystemError = ErrorCode{Code: 515001, Msg: "系统错误"}
+ // InsufficientCredit 这个不管说是客户端错误还是服务端错误,都有点勉强,所以随便用一个 5
+ InsufficientCredit = ErrorCode{Code: 515002, Msg: "积分不足"}
)
type ErrorCode struct {
diff --git a/internal/resume/internal/integration/analysis_handler_test.go b/internal/resume/internal/integration/analysis_handler_test.go
new file mode 100644
index 00000000..8009c407
--- /dev/null
+++ b/internal/resume/internal/integration/analysis_handler_test.go
@@ -0,0 +1,129 @@
+package integration
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/ecodeclub/ekit/iox"
+ "github.com/ecodeclub/ginx/session"
+ "github.com/ecodeclub/webook/internal/ai"
+ aimocks "github.com/ecodeclub/webook/internal/ai/mocks"
+ "github.com/ecodeclub/webook/internal/cases"
+ "github.com/ecodeclub/webook/internal/resume/internal/domain"
+ "github.com/ecodeclub/webook/internal/resume/internal/integration/startup"
+ "github.com/ecodeclub/webook/internal/resume/internal/web"
+ "github.com/ecodeclub/webook/internal/test"
+ "github.com/gin-gonic/gin"
+ "github.com/gotomicro/ego/core/econf"
+ "github.com/gotomicro/ego/server/egin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/mock/gomock"
+)
+
+type AnalysisTestSuite struct {
+ suite.Suite
+ server *egin.Component
+}
+
+func (a *AnalysisTestSuite) SetupSuite() {
+ ctrl := gomock.NewController(a.T())
+ aiSvc := aimocks.NewMockService(ctrl)
+ aiSvc.EXPECT().Invoke(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req ai.LLMRequest) (ai.LLMResponse, error) {
+ switch req.Biz {
+ case domain.BizResumeSkillKeyPoints:
+ return ai.LLMResponse{
+ Tokens: 100,
+ Amount: 100,
+ Answer: fmt.Sprintf("skill的keypoints %s", req.Input[0]),
+ }, nil
+ case domain.BizSkillsRewrite:
+ return ai.LLMResponse{
+ Tokens: 200,
+ Amount: 200,
+ Answer: fmt.Sprintf("%s:%s", req.Input[0], req.Input[1]),
+ }, nil
+ case domain.BizResumeProjectEvaluation:
+ return ai.LLMResponse{
+ Tokens: 150,
+ Amount: 150,
+ Answer: fmt.Sprintf("project的评估 %s", req.Input[0]),
+ }, nil
+ case domain.BizResumeProjectRewrite:
+ return ai.LLMResponse{
+ Tokens: 220,
+ Amount: 220,
+ Answer: fmt.Sprintf("project的重写 %s", req.Input[0]),
+ }, nil
+ case domain.BizResumeJobsRewrite:
+ return ai.LLMResponse{
+ Tokens: 400,
+ Amount: 400,
+ Answer: fmt.Sprintf("工作经历重写%s", req.Input[0]),
+ }, nil
+ default:
+ return ai.LLMResponse{}, errors.New("mock Err")
+ }
+ }).AnyTimes()
+ module := startup.InitModule(&cases.Module{}, &ai.Module{Svc: aiSvc})
+
+ hdl := module.AnalysisHandler
+ econf.Set("server", map[string]any{"contextTimeout": "1s"})
+ server := egin.Load("server").Build()
+ server.Use(func(ctx *gin.Context) {
+ ctx.Set(session.CtxSessionKey,
+ session.NewMemorySession(session.Claims{
+ Uid: uid,
+ }))
+ })
+ hdl.MemberRoutes(server.Engine)
+ a.server = server
+}
+
+func (a *AnalysisTestSuite) TestAnalysis() {
+ testCases := []struct {
+ name string
+
+ req web.AnalysisReq
+
+ wantCode int
+ wantResp test.Result[web.AnalysisResp]
+ }{
+ {
+ name: "分析简历",
+ req: web.AnalysisReq{
+ Resume: "resume",
+ },
+ wantCode: 200,
+ wantResp: test.Result[web.AnalysisResp]{
+ Data: web.AnalysisResp{
+ Amount: 1070,
+ RewriteSkills: ":skill的keypoints resume",
+ RewriteJobs: "工作经历重写resume",
+ RewriteProject: "project的重写 resume\n## 综合评价\nproject的评估 project的重写 resume",
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ a.T().Run(tc.name, func(t *testing.T) {
+ req, err := http.NewRequest(http.MethodPost,
+ "/resume/analysis", iox.NewJSONReader(tc.req))
+ req.Header.Set("content-type", "application/json")
+ require.NoError(t, err)
+ recorder := test.NewJSONResponseRecorder[web.AnalysisResp]()
+ a.server.ServeHTTP(recorder, req)
+ require.Equal(t, tc.wantCode, recorder.Code)
+ data := recorder.MustScan()
+ assert.Equal(t, tc.wantResp, data)
+ })
+ }
+}
+
+func TestAnalysisModule(t *testing.T) {
+ suite.Run(t, new(AnalysisTestSuite))
+}
diff --git a/internal/resume/internal/integration/experience_handler_test.go b/internal/resume/internal/integration/experience_handler_test.go
index 8989849d..a10010f8 100644
--- a/internal/resume/internal/integration/experience_handler_test.go
+++ b/internal/resume/internal/integration/experience_handler_test.go
@@ -6,6 +6,8 @@ import (
"testing"
"time"
+ "github.com/ecodeclub/webook/internal/ai"
+
"github.com/ecodeclub/ekit/iox"
"github.com/ecodeclub/ekit/sqlx"
"github.com/ecodeclub/webook/internal/resume/internal/domain"
@@ -62,7 +64,9 @@ func (s *ExperienceTestSuite) SetupSuite() {
module := startup.InitModule(&cases.Module{
ExamineSvc: examSvc,
- })
+ },
+ &ai.Module{},
+ )
econf.Set("server", map[string]any{"contextTimeout": "1s"})
server := egin.Load("server").Build()
diff --git a/internal/resume/internal/integration/project_handler_test.go b/internal/resume/internal/integration/project_handler_test.go
index 42e41f7b..3dc466a5 100644
--- a/internal/resume/internal/integration/project_handler_test.go
+++ b/internal/resume/internal/integration/project_handler_test.go
@@ -9,6 +9,8 @@ import (
"strconv"
"testing"
+ "github.com/ecodeclub/webook/internal/ai"
+
"github.com/ecodeclub/ekit/iox"
"github.com/ecodeclub/ekit/slice"
"github.com/ecodeclub/ginx/session"
@@ -82,7 +84,8 @@ func (s *ProjectTestSuite) SetupSuite() {
module := startup.InitModule(&cases.Module{
ExamineSvc: examSvc,
Svc: caseSvc,
- })
+ },
+ &ai.Module{})
econf.Set("server", map[string]any{"contextTimeout": "1s"})
server := egin.Load("server").Build()
server.Use(func(ctx *gin.Context) {
@@ -1052,7 +1055,7 @@ func (s *ProjectTestSuite) TestResumeInfo() {
}
}
-func (s *ProjectTestSuite) TestReaumeList() {
+func (s *ProjectTestSuite) TestResumeList() {
for i := 1; i < 4; i++ {
_, err := s.pdao.Upsert(context.Background(), dao.ResumeProject{
ID: int64(i),
@@ -1113,8 +1116,8 @@ func (s *ProjectTestSuite) TestReaumeList() {
})
req, err := http.NewRequest(http.MethodPost,
"/resume/project/list", iox.NewJSONReader(nil))
- req.Header.Set("content-type", "application/json")
require.NoError(s.T(), err)
+ req.Header.Set("content-type", "application/json")
recorder := test.NewJSONResponseRecorder[[]web.Project]()
s.server.ServeHTTP(recorder, req)
diff --git a/internal/resume/internal/integration/startup/wire.go b/internal/resume/internal/integration/startup/wire.go
index 099d04fd..434fc142 100644
--- a/internal/resume/internal/integration/startup/wire.go
+++ b/internal/resume/internal/integration/startup/wire.go
@@ -3,6 +3,7 @@
package startup
import (
+ "github.com/ecodeclub/webook/internal/ai"
"github.com/ecodeclub/webook/internal/cases"
"github.com/ecodeclub/webook/internal/resume"
"github.com/ecodeclub/webook/internal/resume/internal/repository"
@@ -13,7 +14,7 @@ import (
"github.com/google/wire"
)
-func InitModule(caModule *cases.Module) *resume.Module {
+func InitModule(caModule *cases.Module, aiModule *ai.Module) *resume.Module {
wire.Build(
testioc.InitDB,
dao.NewResumeProjectDAO,
@@ -22,10 +23,13 @@ func InitModule(caModule *cases.Module) *resume.Module {
repository.NewExperience,
service.NewService,
service.NewExperienceService,
+ service.NewAnalysisService,
wire.FieldsOf(new(*cases.Module), "ExamineSvc"),
wire.FieldsOf(new(*cases.Module), "Svc"),
+ wire.FieldsOf(new(*ai.Module), "Svc"),
web.NewHandler,
web.NewExperienceHandler,
+ web.NewAnalysisHandler,
wire.Struct(new(resume.Module), "*"),
)
return new(resume.Module)
diff --git a/internal/resume/internal/integration/startup/wire_gen.go b/internal/resume/internal/integration/startup/wire_gen.go
index b4558380..594fe2dc 100644
--- a/internal/resume/internal/integration/startup/wire_gen.go
+++ b/internal/resume/internal/integration/startup/wire_gen.go
@@ -7,6 +7,7 @@
package startup
import (
+ "github.com/ecodeclub/webook/internal/ai"
"github.com/ecodeclub/webook/internal/cases"
"github.com/ecodeclub/webook/internal/resume"
"github.com/ecodeclub/webook/internal/resume/internal/repository"
@@ -18,7 +19,7 @@ import (
// Injectors from wire.go:
-func InitModule(caModule *cases.Module) *resume.Module {
+func InitModule(caModule *cases.Module, aiModule *ai.Module) *resume.Module {
db := testioc.InitDB()
resumeProjectDAO := dao.NewResumeProjectDAO(db)
resumeProjectRepo := repository.NewResumeProjectRepo(resumeProjectDAO)
@@ -30,9 +31,13 @@ func InitModule(caModule *cases.Module) *resume.Module {
experience := repository.NewExperience(experienceDAO)
experienceService := service.NewExperienceService(experience)
experienceHandler := web.NewExperienceHandler(experienceService)
+ llmService := aiModule.Svc
+ analysisService := service.NewAnalysisService(llmService)
+ analysisHandler := web.NewAnalysisHandler(analysisService)
module := &resume.Module{
- PrjHdl: projectHandler,
- ExperienceHdl: experienceHandler,
+ PrjHdl: projectHandler,
+ ExperienceHdl: experienceHandler,
+ AnalysisHandler: analysisHandler,
}
return module
}
diff --git a/internal/resume/internal/service/analysis.go b/internal/resume/internal/service/analysis.go
new file mode 100644
index 00000000..781b3091
--- /dev/null
+++ b/internal/resume/internal/service/analysis.go
@@ -0,0 +1,185 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "sync/atomic"
+
+ "github.com/ecodeclub/webook/internal/ai"
+ "github.com/ecodeclub/webook/internal/resume/internal/domain"
+ "github.com/lithammer/shortuuid/v4"
+ "golang.org/x/sync/errgroup"
+)
+
+var ErrInsufficientCredit = ai.ErrInsufficientCredit
+
+type AnalysisService interface {
+ Analysis(ctx context.Context, uid int64, resume string) (domain.ResumeAnalysis, error)
+}
+
+type analysisService struct {
+ aiSvc ai.LLMService
+}
+
+func NewAnalysisService(aiSvc ai.LLMService) AnalysisService {
+ return &analysisService{
+ aiSvc: aiSvc,
+ }
+}
+
+func (r *analysisService) Analysis(ctx context.Context, uid int64, resume string) (domain.ResumeAnalysis, error) {
+ tid := shortuuid.New()
+ var eg errgroup.Group
+
+ var amount int64
+ var rewriteSKills, rewriteProject, rewriteJobs string
+ // 重写技能
+ eg.Go(func() error {
+ keyPointsAmount, keyPoints, err := r.skillKeypoint(ctx, uid, fmt.Sprintf("%s_skills_get_keypoints", tid), resume)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, keyPointsAmount)
+ // 暂时不需要传入原始简历,不然会严重超时,并且上下文太长,搞崩系统
+ rewriteSkillsAmount, ans, err := r.rewriteSkills(ctx, uid, fmt.Sprintf("%s_skills_rewrite", tid), keyPoints, "")
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, rewriteSkillsAmount)
+ rewriteSKills = ans
+ return nil
+ })
+ // 重写项目
+ eg.Go(func() error {
+ rewriteProjectAmount, ans, err := r.rewriteProject(ctx, uid, fmt.Sprintf("%s_project_rewrite", tid), resume)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, rewriteProjectAmount)
+ evaluationAmt, evaluation, err := r.evaluatePrj(ctx, uid,
+ domain.BizResumeProjectEvaluation,
+ fmt.Sprintf("%s_project_get_evaludation", tid), ans)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, evaluationAmt)
+ rewriteProject = fmt.Sprintf("%s\n## 综合评价\n%s", ans, evaluation)
+ return nil
+ })
+ // 重写工作经历
+ eg.Go(func() error {
+ // 暂时还不需要提取关键字
+ //keyPointsAmount, keyPoints, err := r.evaluatePrj(ctx, uid, domain.BizResumeJobsKeyPoints, fmt.Sprintf("%s_jobs_get_keypoints", tid), resume)
+ //if err != nil {
+ // return err
+ //}
+ //atomic.AddInt64(&amount, keyPointsAmount)
+ rewriteJobsAmount, ans, err := r.rewriteJobs(ctx, uid, fmt.Sprintf("%s_jobs_rewrite", tid), "", resume)
+ if err != nil {
+ return err
+ }
+ atomic.AddInt64(&amount, rewriteJobsAmount)
+ rewriteJobs = ans
+ return nil
+ })
+
+ if err := eg.Wait(); err != nil {
+ return domain.ResumeAnalysis{}, err
+ }
+
+ return domain.ResumeAnalysis{
+ Amount: amount,
+ RewriteSkills: rewriteSKills,
+ RewriteProject: rewriteProject,
+ RewriteJobs: rewriteJobs,
+ }, nil
+
+}
+
+func (r *analysisService) evaluatePrj(ctx context.Context, uid int64, biz, tid, rewritePrj string) (int64, string, error) {
+ aiReq := ai.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: biz,
+ // 标题,标准答案,输入
+ Input: []string{rewritePrj},
+ }
+ resp, err := r.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, "", err
+ }
+ return resp.Amount, resp.Answer, nil
+}
+
+// 提取关键字
+func (r *analysisService) skillKeypoint(ctx context.Context, uid int64, tid, resume string) (int64, string, error) {
+ aiReq := ai.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: domain.BizResumeSkillKeyPoints,
+ Input: []string{resume},
+ }
+ resp, err := r.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, "", err
+ }
+ return resp.Amount, resp.Answer, nil
+}
+
+// 重写技能
+func (r *analysisService) rewriteSkills(ctx context.Context, uid int64, tid, keyPoints, resume string) (int64, string, error) {
+ aiReq := ai.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: domain.BizSkillsRewrite,
+ // 标题,标准答案,输入
+ Input: []string{
+ // 简历
+ resume,
+ // 前一步提取的关键字
+ keyPoints,
+ },
+ }
+ resp, err := r.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, "", err
+ }
+ return resp.Amount, resp.Answer, nil
+}
+
+// 重写项目
+func (r *analysisService) rewriteProject(ctx context.Context, uid int64, tid, resume string) (int64, string, error) {
+ aiReq := ai.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: domain.BizResumeProjectRewrite,
+ // 标题,标准答案,输入
+ Input: []string{
+ resume,
+ },
+ }
+ resp, err := r.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, "", err
+ }
+ return resp.Amount, resp.Answer, nil
+}
+
+// 重写工作经历
+func (r *analysisService) rewriteJobs(ctx context.Context, uid int64, tid, keyPoints, resume string) (int64, string, error) {
+ aiReq := ai.LLMRequest{
+ Uid: uid,
+ Tid: tid,
+ Biz: domain.BizResumeJobsRewrite,
+ // 标题,标准答案,输入
+ Input: []string{
+ resume,
+ keyPoints,
+ },
+ }
+ resp, err := r.aiSvc.Invoke(ctx, aiReq)
+ if err != nil {
+ return 0, "", err
+ }
+ return resp.Amount, resp.Answer, nil
+}
diff --git a/internal/resume/internal/web/analysis.go b/internal/resume/internal/web/analysis.go
new file mode 100644
index 00000000..7e403b61
--- /dev/null
+++ b/internal/resume/internal/web/analysis.go
@@ -0,0 +1,49 @@
+package web
+
+import (
+ "errors"
+
+ "github.com/ecodeclub/ginx"
+ "github.com/ecodeclub/ginx/session"
+ "github.com/ecodeclub/webook/internal/resume/internal/errs"
+ "github.com/ecodeclub/webook/internal/resume/internal/service"
+ "github.com/gin-gonic/gin"
+)
+
+type AnalysisHandler struct {
+ svc service.AnalysisService
+}
+
+func NewAnalysisHandler(svc service.AnalysisService) *AnalysisHandler {
+ return &AnalysisHandler{
+ svc: svc,
+ }
+}
+
+func (h *AnalysisHandler) MemberRoutes(server *gin.Engine) {
+ g := server.Group("/resume/analysis")
+ g.POST("", ginx.BS(h.Analysis))
+}
+
+func (h *AnalysisHandler) Analysis(ctx *ginx.Context, req AnalysisReq, sess session.Session) (ginx.Result, error) {
+ analysis, err := h.svc.Analysis(ctx, sess.Claims().Uid, req.Resume)
+ switch {
+ case errors.Is(err, service.ErrInsufficientCredit):
+ return ginx.Result{
+ Code: errs.InsufficientCredit.Code,
+ Msg: errs.InsufficientCredit.Msg,
+ }, nil
+
+ case err == nil:
+ return ginx.Result{
+ Data: AnalysisResp{
+ Amount: analysis.Amount,
+ RewriteProject: analysis.RewriteProject,
+ RewriteSkills: analysis.RewriteSkills,
+ RewriteJobs: analysis.RewriteJobs,
+ },
+ }, nil
+ default:
+ return systemErrorResult, err
+ }
+}
diff --git a/internal/resume/internal/web/project.go b/internal/resume/internal/web/project.go
index 3a1bb552..e8405203 100644
--- a/internal/resume/internal/web/project.go
+++ b/internal/resume/internal/web/project.go
@@ -113,7 +113,7 @@ func (h *ProjectHandler) ProjectInfo(ctx *ginx.Context, req IDItem, sess session
func (h *ProjectHandler) ProjectList(ctx *ginx.Context, sess session.Session) (ginx.Result, error) {
uid := sess.Claims().Uid
- projects, err := h.svc.FindProjects(ctx.Request.Context(), uid)
+ projects, err := h.svc.FindProjects(ctx, uid)
if err != nil {
return systemErrorResult, err
}
@@ -184,13 +184,14 @@ func (h *ProjectHandler) getCaMap(ctx *ginx.Context, uid int64, cids []int64) (m
caMap map[int64]cases.Case
eg errgroup.Group
)
+ pctx := ctx.Request.Context()
eg.Go(func() error {
var eerr error
- resMap, eerr = h.examSvc.GetResults(ctx, uid, cids)
+ resMap, eerr = h.examSvc.GetResults(pctx, uid, cids)
return eerr
})
eg.Go(func() error {
- cas, eerr := h.caseSvc.GetPubByIDs(ctx, cids)
+ cas, eerr := h.caseSvc.GetPubByIDs(pctx, cids)
if eerr != nil {
return eerr
}
diff --git a/internal/resume/internal/web/vo.go b/internal/resume/internal/web/vo.go
index 9370f800..c98344c6 100644
--- a/internal/resume/internal/web/vo.go
+++ b/internal/resume/internal/web/vo.go
@@ -70,6 +70,16 @@ type Difficulty struct {
// 适合用作难点的基本方案
Case Case `json:"case"`
}
+type AnalysisReq struct {
+ Resume string `json:"resume"`
+}
+
+type AnalysisResp struct {
+ Amount int64 `json:"amount"`
+ RewriteSkills string `json:"rewriteSkills"`
+ RewriteProject string `json:"rewriteProject"`
+ RewriteJobs string `json:"rewriteJobs"`
+}
func newProject(project domain.Project, examMap map[int64]cases.ExamineResult, caseMap map[int64]cases.Case) Project {
return Project{
diff --git a/internal/resume/module.go b/internal/resume/module.go
index cf933542..db4fefb6 100644
--- a/internal/resume/module.go
+++ b/internal/resume/module.go
@@ -18,8 +18,10 @@ import "github.com/ecodeclub/webook/internal/resume/internal/web"
type ExperienceHandler = web.ExperienceHandler
type ProjectHandler = web.ProjectHandler
+type AnalysisHandler = web.AnalysisHandler
type Module struct {
- PrjHdl *ProjectHandler
- ExperienceHdl *ExperienceHandler
+ PrjHdl *ProjectHandler
+ ExperienceHdl *ExperienceHandler
+ AnalysisHandler *AnalysisHandler
}
diff --git a/internal/resume/wire.go b/internal/resume/wire.go
index be789787..c867a6ca 100644
--- a/internal/resume/wire.go
+++ b/internal/resume/wire.go
@@ -19,6 +19,8 @@ package resume
import (
"sync"
+ "github.com/ecodeclub/webook/internal/ai"
+
"github.com/ecodeclub/webook/internal/cases"
"github.com/ecodeclub/webook/internal/resume/internal/repository"
"github.com/ecodeclub/webook/internal/resume/internal/repository/dao"
@@ -28,14 +30,21 @@ import (
"github.com/google/wire"
)
-func InitModule(db *egorm.Component, caModule *cases.Module) *Module {
+func InitModule(db *egorm.Component, caModule *cases.Module, aiModule *ai.Module) *Module {
wire.Build(
initResumeProjectDAOOnce,
+ dao.NewExperienceDAO,
repository.NewResumeProjectRepo,
+ repository.NewExperience,
+ service.NewExperienceService,
service.NewService,
wire.FieldsOf(new(*cases.Module), "ExamineSvc"),
wire.FieldsOf(new(*cases.Module), "Svc"),
+ wire.FieldsOf(new(*ai.Module), "Svc"),
+ service.NewAnalysisService,
web.NewHandler,
+ web.NewAnalysisHandler,
+ web.NewExperienceHandler,
wire.Struct(new(Module), "*"),
)
return new(Module)
diff --git a/internal/resume/wire_gen.go b/internal/resume/wire_gen.go
index 82e31b4f..3f29acd2 100644
--- a/internal/resume/wire_gen.go
+++ b/internal/resume/wire_gen.go
@@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
-//go:generate go run github.com/google/wire/cmd/wire
+//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
@@ -9,6 +9,7 @@ package resume
import (
"sync"
+ "github.com/ecodeclub/webook/internal/ai"
"github.com/ecodeclub/webook/internal/cases"
"github.com/ecodeclub/webook/internal/resume/internal/repository"
"github.com/ecodeclub/webook/internal/resume/internal/repository/dao"
@@ -20,15 +21,24 @@ import (
// Injectors from wire.go:
-func InitModule(db *gorm.DB, caModule *cases.Module) *Module {
+func InitModule(db *gorm.DB, caModule *cases.Module, aiModule *ai.Module) *Module {
daoResumeProjectDAO := initResumeProjectDAOOnce(db)
resumeProjectRepo := repository.NewResumeProjectRepo(daoResumeProjectDAO)
serviceService := service.NewService(resumeProjectRepo)
examineService := caModule.ExamineSvc
service2 := caModule.Svc
projectHandler := web.NewHandler(serviceService, examineService, service2)
+ experienceDAO := dao.NewExperienceDAO(db)
+ experience := repository.NewExperience(experienceDAO)
+ experienceService := service.NewExperienceService(experience)
+ experienceHandler := web.NewExperienceHandler(experienceService)
+ llmService := aiModule.Svc
+ analysisService := service.NewAnalysisService(llmService)
+ analysisHandler := web.NewAnalysisHandler(analysisService)
module := &Module{
- PrjHdl: projectHandler,
+ PrjHdl: projectHandler,
+ ExperienceHdl: experienceHandler,
+ AnalysisHandler: analysisHandler,
}
return module
}
diff --git a/ioc/admin.go b/ioc/admin.go
index cb750ee4..aba8d06c 100644
--- a/ioc/admin.go
+++ b/ioc/admin.go
@@ -18,6 +18,8 @@ import (
"net/http"
"strings"
+ "github.com/ecodeclub/webook/internal/ai"
+
"github.com/ecodeclub/webook/internal/cases"
baguwen "github.com/ecodeclub/webook/internal/question"
@@ -43,7 +45,9 @@ func InitAdminServer(prj *project.AdminHandler,
queSet *baguwen.AdminQuestionSetHandler,
caseHdl *cases.AdminCaseHandler,
caseSetHdl *cases.AdminCaseSetHandler,
- mark *marketing.AdminHandler) AdminServer {
+ mark *marketing.AdminHandler,
+ aiHdl *ai.AdminHandler,
+) AdminServer {
res := egin.Load("admin").Build()
res.Use(cors.New(cors.Config{
ExposeHeaders: []string{"X-Refresh-Token", "X-Access-Token"},
@@ -73,6 +77,7 @@ func InitAdminServer(prj *project.AdminHandler,
que.PrivateRoutes(res.Engine)
caseHdl.PrivateRoutes(res.Engine)
caseSetHdl.PrivateRoutes(res.Engine)
+ aiHdl.RegisterRoutes(res.Engine)
return res
}
diff --git a/ioc/gin.go b/ioc/gin.go
index 64e024bf..2b3a80c6 100644
--- a/ioc/gin.go
+++ b/ioc/gin.go
@@ -18,6 +18,8 @@ import (
"net/http"
"strings"
+ "github.com/ecodeclub/webook/internal/ai"
+
"github.com/ecodeclub/webook/internal/resume"
"github.com/ecodeclub/webook/internal/bff"
@@ -86,6 +88,8 @@ func initGinxServer(sp session.Provider,
csHdl *cases.CaseSetHandler,
caseExamineHdl *cases.ExamineHandler,
resumePrjHdl *resume.ProjectHandler,
+ resumeAnaHdl *resume.AnalysisHandler,
+ aiHdl *ai.LLMHandler,
) *egin.Component {
session.SetDefaultProvider(sp)
res := egin.Load("web").Build()
@@ -159,5 +163,7 @@ func initGinxServer(sp session.Provider,
skillHdl.MemberRoutes(res.Engine)
caseExamineHdl.MemberRoutes(res.Engine)
resumePrjHdl.MemberRoutes(res.Engine)
+ resumeAnaHdl.MemberRoutes(res.Engine)
+ aiHdl.MemberRoutes(res.Engine)
return res
}
diff --git a/ioc/wire.go b/ioc/wire.go
index 67114711..2bf39031 100644
--- a/ioc/wire.go
+++ b/ioc/wire.go
@@ -91,7 +91,9 @@ func InitApp() (*App, error) {
bff.InitModule,
wire.FieldsOf(new(*bff.Module), "Hdl"),
resume.InitModule,
- wire.FieldsOf(new(*resume.Module), "PrjHdl"),
+ wire.FieldsOf(new(*resume.Module), "PrjHdl", "AnalysisHandler"),
+ wire.FieldsOf(new(*ai.Module), "Hdl", "AdminHandler"),
+
initLocalActiveLimiterBuilder,
initCronJobs,
// 这两个顺序不要换
diff --git a/ioc/wire_gen.go b/ioc/wire_gen.go
index 16bf3130..9234ffe3 100644
--- a/ioc/wire_gen.go
+++ b/ioc/wire_gen.go
@@ -131,9 +131,11 @@ func InitApp() (*App, error) {
handler16 := bffModule.Hdl
caseSetHandler := casesModule.CsHdl
webExamineHandler := casesModule.ExamineHdl
- resumeModule := resume.InitModule(db, casesModule)
+ resumeModule := resume.InitModule(db, casesModule, aiModule)
projectHandler := resumeModule.PrjHdl
- component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, examineHandler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14, handler15, handler16, caseSetHandler, webExamineHandler, projectHandler)
+ analysisHandler := resumeModule.AnalysisHandler
+ handler17 := aiModule.Hdl
+ component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, examineHandler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14, handler15, handler16, caseSetHandler, webExamineHandler, projectHandler, analysisHandler, handler17)
adminHandler := projectModule.AdminHdl
webAdminHandler := roadmapModule.AdminHdl
adminHandler2 := baguwenModule.AdminHdl
@@ -141,7 +143,8 @@ func InitApp() (*App, error) {
adminCaseHandler := casesModule.AdminHandler
adminCaseSetHandler := casesModule.AdminSetHandler
adminHandler3 := marketingModule.AdminHdl
- adminServer := InitAdminServer(adminHandler, webAdminHandler, adminHandler2, adminQuestionSetHandler, adminCaseHandler, adminCaseSetHandler, adminHandler3)
+ adminHandler4 := aiModule.AdminHandler
+ adminServer := InitAdminServer(adminHandler, webAdminHandler, adminHandler2, adminQuestionSetHandler, adminCaseHandler, adminCaseSetHandler, adminHandler3, adminHandler4)
closeTimeoutOrdersJob := orderModule.CloseTimeoutOrdersJob
closeTimeoutLockedCreditsJob := creditModule.CloseTimeoutLockedCreditsJob
syncWechatOrderJob := paymentModule.SyncWechatOrderJob