增加任务管理模块
This commit is contained in:
@@ -194,9 +194,9 @@ func (c *AuthController) Login() {
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: usernameForToken,
|
||||
Module: "auth",
|
||||
Module: "登录模块",
|
||||
ResourceType: "user",
|
||||
Operation: "LOGIN",
|
||||
Operation: "登录",
|
||||
IpAddress: clientIP,
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: "POST",
|
||||
|
||||
@@ -2,6 +2,8 @@ package controllers
|
||||
|
||||
import (
|
||||
"server/models"
|
||||
"server/services"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
@@ -106,3 +108,104 @@ func (c *DashboardController) GetTenantStats() {
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserActivityLogs 获取当前用户的最新活动日志(包含操作日志和登录日志)
|
||||
// @router /api/dashboard/user-activity-logs [get]
|
||||
func (c *DashboardController) GetUserActivityLogs() {
|
||||
// 获取当前用户ID和租户ID
|
||||
userId := 0
|
||||
if userIdVal, ok := c.Ctx.Input.GetData("userId").(int); ok {
|
||||
userId = userIdVal
|
||||
}
|
||||
|
||||
tenantId := 0
|
||||
if tenantIdVal, ok := c.Ctx.Input.GetData("tenantId").(int); ok {
|
||||
tenantId = tenantIdVal
|
||||
}
|
||||
|
||||
if userId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "无法获取用户信息",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
pageNum, _ := c.GetInt("page_num", 1)
|
||||
pageSize, _ := c.GetInt("page_size", 10)
|
||||
logType := c.GetString("type") // "operation" 或 "access" 或 "all"
|
||||
operation := c.GetString("operation") // 操作类型过滤
|
||||
|
||||
if logType == "" {
|
||||
logType = "all"
|
||||
}
|
||||
|
||||
type ActivityLog struct {
|
||||
Id int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Username string `json:"username"`
|
||||
Module string `json:"module"`
|
||||
Operation string `json:"operation"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
var logs []ActivityLog
|
||||
|
||||
// 获取操作日志
|
||||
if logType == "operation" || logType == "all" {
|
||||
operationLogs, _, err := services.GetOperationLogs(tenantId, userId, "", operation, nil, nil, pageNum, pageSize)
|
||||
if err == nil && operationLogs != nil {
|
||||
for _, log := range operationLogs {
|
||||
logs = append(logs, ActivityLog{
|
||||
Id: log.Id,
|
||||
Type: "operation",
|
||||
Username: log.Username,
|
||||
Module: log.Module,
|
||||
Operation: log.Operation,
|
||||
Action: log.Operation,
|
||||
Description: log.Description,
|
||||
Timestamp: log.CreateTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取访问/登录日志
|
||||
if logType == "access" || logType == "all" {
|
||||
accessLogs, _, err := services.GetAccessLogs(tenantId, userId, "", "", nil, nil, pageNum, pageSize)
|
||||
if err == nil && accessLogs != nil {
|
||||
for _, log := range accessLogs {
|
||||
logs = append(logs, ActivityLog{
|
||||
Id: log.Id,
|
||||
Type: "access",
|
||||
Username: log.Username,
|
||||
Module: log.Module,
|
||||
Operation: log.RequestMethod,
|
||||
Action: "访问",
|
||||
Description: "访问 " + log.Module,
|
||||
Timestamp: log.CreateTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间戳排序(最新的在前)
|
||||
if len(logs) > pageSize {
|
||||
logs = logs[:pageSize]
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": map[string]interface{}{
|
||||
"logs": logs,
|
||||
"total": len(logs),
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
@@ -245,3 +246,193 @@ func (c *OperationLogController) ClearOldLogs() {
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetAccessLogs 获取访问日志列表
|
||||
func (c *OperationLogController) GetAccessLogs() {
|
||||
// 获取租户ID和用户ID
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
pageNum, _ := c.GetInt("page_num", 1)
|
||||
pageSize, _ := c.GetInt("page_size", 20)
|
||||
module := c.GetString("module")
|
||||
resourceType := c.GetString("resource_type")
|
||||
startTimeStr := c.GetString("start_time")
|
||||
endTimeStr := c.GetString("end_time")
|
||||
|
||||
var startTime, endTime *time.Time
|
||||
|
||||
if startTimeStr != "" {
|
||||
if t, err := time.Parse("2006-01-02", startTimeStr); err == nil {
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
if endTimeStr != "" {
|
||||
if t, err := time.Parse("2006-01-02", endTimeStr); err == nil {
|
||||
endTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// 查询访问日志
|
||||
logs, total, err := services.GetAccessLogs(tenantId, userId, module, resourceType, startTime, endTime, pageNum, pageSize)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询访问日志失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"logs": logs,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetAccessLogById 根据ID获取访问日志详情
|
||||
func (c *OperationLogController) GetAccessLogById() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的日志ID",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
log, err := services.GetAccessLogById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询日志详情失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
var user *models.User
|
||||
if log.UserId > 0 {
|
||||
user, _ = models.GetUserById(log.UserId)
|
||||
}
|
||||
|
||||
// 构造返回数据
|
||||
result := map[string]interface{}{
|
||||
"id": log.Id,
|
||||
"tenant_id": log.TenantId,
|
||||
"user_id": log.UserId,
|
||||
"username": log.Username,
|
||||
"module": log.Module,
|
||||
"resource_type": log.ResourceType,
|
||||
"resource_id": log.ResourceId,
|
||||
"request_url": log.RequestUrl,
|
||||
"query_string": log.QueryString,
|
||||
"ip_address": log.IpAddress,
|
||||
"user_agent": log.UserAgent,
|
||||
"request_method": log.RequestMethod,
|
||||
"duration": log.Duration,
|
||||
"create_time": log.CreateTime,
|
||||
}
|
||||
|
||||
// 如果有用户信息,添加到结果中
|
||||
if user != nil {
|
||||
result["user_nickname"] = user.Nickname
|
||||
result["user_email"] = user.Email
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": result,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserAccessStats 获取用户访问统计
|
||||
func (c *OperationLogController) GetUserAccessStats() {
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
days, _ := c.GetInt("days", 7)
|
||||
|
||||
stats, err := services.GetUserAccessStats(tenantId, userId, days)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询统计失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": stats,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ClearOldAccessLogs 清空旧访问日志
|
||||
func (c *OperationLogController) ClearOldAccessLogs() {
|
||||
// 获取参数
|
||||
var params map[string]interface{}
|
||||
if err := c.ParseForm(¶ms); err != nil {
|
||||
// 如果ParseForm失败,尝试解析JSON
|
||||
c.Ctx.Input.Bind(¶ms, "json")
|
||||
}
|
||||
|
||||
keepDays := 90 // 默认保留90天
|
||||
if kd, ok := params["keep_days"].(float64); ok {
|
||||
keepDays = int(kd)
|
||||
}
|
||||
|
||||
rowsAffected, err := services.ClearOldAccessLogs(keepDays)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "清空旧日志失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("清空旧日志成功,共删除 %d 条记录", rowsAffected),
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// TaskController OA任务管理控制器
|
||||
type TaskController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// GetTasks 列表查询
|
||||
// @router /api/oa/tasks [get]
|
||||
func (c *TaskController) GetTasks() {
|
||||
// 获取租户ID(JWT中间件写入)
|
||||
tenantId := 0
|
||||
if v := c.Ctx.Input.GetData("tenantId"); v != nil {
|
||||
if tid, ok := v.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
// 允许请求参数覆盖(如有)
|
||||
if reqTenantId, err := c.GetInt("tenant_id"); err == nil && reqTenantId > 0 {
|
||||
tenantId = reqTenantId
|
||||
}
|
||||
if tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
keyword := c.GetString("keyword")
|
||||
status := c.GetString("status")
|
||||
priority := c.GetString("priority")
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
|
||||
items, total, err := services.ListOATasks(tenantId, keyword, status, priority, page, pageSize)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "获取任务列表失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取成功",
|
||||
"data": map[string]interface{}{
|
||||
"list": items,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTaskById 详情
|
||||
// @router /api/oa/tasks/:id [get]
|
||||
func (c *TaskController) GetTaskById() {
|
||||
id, err := c.GetInt(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "任务ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
t, err := services.GetOATaskById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "获取任务详情失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取成功",
|
||||
"data": t,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateTask 新增
|
||||
// @router /api/oa/tasks [post]
|
||||
func (c *TaskController) CreateTask() {
|
||||
var t models.Task
|
||||
body := c.Ctx.Input.RequestBody
|
||||
|
||||
// 先解析 team_employee_ids
|
||||
var extra struct {
|
||||
TeamEmployeeIds []int64 `json:"team_employee_ids"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &extra)
|
||||
|
||||
// 去除 team_employee_ids 字段后再解析为 Task,避免 array->string 报错
|
||||
clean := map[string]interface{}{}
|
||||
if err := json.Unmarshal(body, &clean); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
delete(clean, "team_employee_ids")
|
||||
buf, _ := json.Marshal(clean)
|
||||
if err := json.Unmarshal(buf, &t); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if len(extra.TeamEmployeeIds) > 0 {
|
||||
ids := make([]string, 0, len(extra.TeamEmployeeIds))
|
||||
for _, id := range extra.TeamEmployeeIds {
|
||||
ids = append(ids, strconv.FormatInt(id, 10))
|
||||
}
|
||||
t.TeamEmployeeIds = strings.Join(ids, ",")
|
||||
}
|
||||
|
||||
// 从上下文取租户和用户名交给服务层处理默认值
|
||||
tenantId := 0
|
||||
if v := c.Ctx.Input.GetData("tenantId"); v != nil {
|
||||
if tid, ok := v.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
username := ""
|
||||
if u, ok := c.Ctx.Input.GetData("username").(string); ok {
|
||||
username = u
|
||||
}
|
||||
|
||||
if err := services.CreateOATask(&t, tenantId, username); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "创建任务失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "创建成功",
|
||||
"data": map[string]interface{}{
|
||||
"id": t.Id,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateTask 更新
|
||||
// @router /api/oa/tasks/:id [put]
|
||||
func (c *TaskController) UpdateTask() {
|
||||
id, err := c.GetInt(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "任务ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 保证存在
|
||||
if _, err := services.GetOATaskById(id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "任务不存在: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var t models.Task
|
||||
body := c.Ctx.Input.RequestBody
|
||||
|
||||
// 先解析 team_employee_ids
|
||||
var extra struct {
|
||||
TeamEmployeeIds []int64 `json:"team_employee_ids"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &extra)
|
||||
|
||||
// 去除 team_employee_ids 字段后再解析为 Task,避免 array->string 报错
|
||||
clean := map[string]interface{}{}
|
||||
if err := json.Unmarshal(body, &clean); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
delete(clean, "team_employee_ids")
|
||||
buf, _ := json.Marshal(clean)
|
||||
if err := json.Unmarshal(buf, &t); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
t.Id = id
|
||||
|
||||
ids := make([]string, 0, len(extra.TeamEmployeeIds))
|
||||
for _, eid := range extra.TeamEmployeeIds {
|
||||
ids = append(ids, strconv.FormatInt(eid, 10))
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
t.TeamEmployeeIds = strings.Join(ids, ",")
|
||||
} else {
|
||||
// 明确传空时,清空关联人
|
||||
t.TeamEmployeeIds = ""
|
||||
}
|
||||
|
||||
// 操作人交由服务层设置
|
||||
username := ""
|
||||
if u, ok := c.Ctx.Input.GetData("username").(string); ok {
|
||||
username = u
|
||||
}
|
||||
|
||||
if err := services.UpdateOATask(&t, username); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "更新任务失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "更新成功",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTask 删除(软删除)
|
||||
// @router /api/oa/tasks/:id [delete]
|
||||
func (c *TaskController) DeleteTask() {
|
||||
id, err := c.GetInt(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "任务ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
operatorName := "system"
|
||||
operatorId := 0
|
||||
if username, ok := c.Ctx.Input.GetData("username").(string); ok && username != "" {
|
||||
operatorName = username
|
||||
}
|
||||
if uid, ok := c.Ctx.Input.GetData("userId").(int); ok && uid > 0 {
|
||||
operatorId = uid
|
||||
}
|
||||
|
||||
if err := services.DeleteOATask(id, operatorName, operatorId); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "删除任务失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "删除成功",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
CREATE TABLE `yz_tenant_tasks` (
|
||||
-- 主键与基础标识
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID(主键)',
|
||||
`tenant_id` bigint NOT NULL COMMENT '租户ID(多租户隔离,如无多租户需求可设为默认1)',
|
||||
`task_no` varchar(64) NOT NULL COMMENT '任务编号(唯一标识,如TASK20251112001)',
|
||||
|
||||
-- 任务核心信息
|
||||
`task_name` varchar(255) NOT NULL COMMENT '任务名称',
|
||||
`task_desc` text COMMENT '任务描述(富文本内容,支持图片/表格)',
|
||||
`task_type` varchar(32) DEFAULT 'common' COMMENT '任务类型(common=普通任务,project=项目任务,repeat=重复任务,可自定义)',
|
||||
`business_tag` varchar(64) COMMENT '业务标签(多个标签用逗号分隔,如"紧急,日常协作")',
|
||||
|
||||
-- 关联信息
|
||||
`parent_task_id` bigint DEFAULT NULL COMMENT '父任务ID(子任务关联用,无父任务则为NULL)',
|
||||
`project_id` bigint DEFAULT NULL COMMENT '关联项目ID(关联OA项目模块)',
|
||||
`related_id` bigint DEFAULT NULL COMMENT '关联其他模块ID(如审批单ID、客户ID)',
|
||||
`related_type` varchar(32) COMMENT '关联模块类型(approval=审批单,customer=客户,为空则无关联)',
|
||||
|
||||
-- 人员配置
|
||||
`creator_id` bigint NOT NULL COMMENT '创建人ID',
|
||||
`creator_name` varchar(64) NOT NULL COMMENT '创建人姓名',
|
||||
`principal_id` bigint NOT NULL COMMENT '负责人ID',
|
||||
`principal_name` varchar(64) NOT NULL COMMENT '负责人姓名',
|
||||
`participant_ids` varchar(512) COMMENT '参与人ID(多个用逗号分隔)',
|
||||
`participant_names` varchar(512) COMMENT '参与人姓名(多个用逗号分隔)',
|
||||
`cc_ids` varchar(512) COMMENT '抄送人ID(多个用逗号分隔)',
|
||||
`cc_names` varchar(512) COMMENT '抄送人姓名(多个用逗号分隔)',
|
||||
|
||||
-- 时间配置
|
||||
`plan_start_time` datetime DEFAULT NULL COMMENT '计划开始时间',
|
||||
`plan_end_time` datetime NOT NULL COMMENT '计划截止时间',
|
||||
`actual_start_time` datetime DEFAULT NULL COMMENT '实际开始时间',
|
||||
`actual_end_time` datetime DEFAULT NULL COMMENT '实际结束时间',
|
||||
`estimated_hours` decimal(10,2) DEFAULT NULL COMMENT '预估工时(小时)',
|
||||
`actual_hours` decimal(10,2) DEFAULT NULL COMMENT '实际工时(小时)',
|
||||
|
||||
-- 状态与优先级
|
||||
`task_status` varchar(32) NOT NULL DEFAULT 'not_started' COMMENT '任务状态(not_started=未开始,in_progress=进行中,paused=暂停,completed=已完成,closed=已关闭,可自定义)',
|
||||
`priority` varchar(16) NOT NULL DEFAULT 'medium' COMMENT '优先级(high=高,medium=中,low=低,urgent=紧急)',
|
||||
`progress` tinyint NOT NULL DEFAULT 0 COMMENT '任务进度(0-100,子任务存在时自动计算)',
|
||||
|
||||
-- 规则与审批配置
|
||||
`need_approval` tinyint NOT NULL DEFAULT 0 COMMENT '是否需要完成审批(0=否,1=是)',
|
||||
`approval_id` bigint DEFAULT NULL COMMENT '关联审批单ID(完成审批时填写)',
|
||||
`delay_approved` tinyint NOT NULL DEFAULT 0 COMMENT '是否已延期审批(0=否,1=是)',
|
||||
`old_plan_end_time` datetime COMMENT '原计划截止时间(延期时记录)',
|
||||
|
||||
-- 重复任务配置
|
||||
`repeat_type` varchar(16) DEFAULT NULL COMMENT '重复类型(daily=按日,weekly=按周,monthly=按月,为空则非重复任务)',
|
||||
`repeat_cycle` int DEFAULT NULL COMMENT '重复周期(如每周重复则为7,每月重复则为30)',
|
||||
`repeat_end_time` datetime DEFAULT NULL COMMENT '重复截止时间(重复任务终止时间)',
|
||||
|
||||
-- 辅助字段
|
||||
`attachment_ids` varchar(1024) COMMENT '附件ID(关联文件表,多个用逗号分隔)',
|
||||
`remark` varchar(512) COMMENT '备注(额外说明)',
|
||||
`is_archived` tinyint NOT NULL DEFAULT 0 COMMENT '是否归档(0=未归档,1=已归档)',
|
||||
`archive_time` datetime COMMENT '归档时间',
|
||||
|
||||
-- 审计字段
|
||||
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除(0=正常,1=删除)',
|
||||
`deleted_time` datetime COMMENT '删除时间',
|
||||
`operator_id` bigint COMMENT '最后操作人ID',
|
||||
`operator_name` varchar(64) COMMENT '最后操作人姓名',
|
||||
|
||||
-- 索引
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_task_no` (`tenant_id`,`task_no`) COMMENT '租户+任务编号唯一索引',
|
||||
KEY `idx_tenant_principal` (`tenant_id`,`principal_id`) COMMENT '租户+负责人索引(查询个人任务)',
|
||||
KEY `idx_tenant_status` (`tenant_id`,`task_status`) COMMENT '租户+状态索引(筛选任务状态)',
|
||||
KEY `idx_tenant_project` (`tenant_id`,`project_id`) COMMENT '租户+项目索引(查询项目下任务)',
|
||||
KEY `idx_tenant_plan_end_time` (`tenant_id`,`plan_end_time`) COMMENT '租户+截止时间索引(逾期提醒、日历视图)',
|
||||
KEY `idx_parent_task_id` (`parent_task_id`) COMMENT '父任务ID索引(查询子任务)'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA系统任务表(多租户适配)';
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MySQL MCP Server 交互式客户端示例
|
||||
可用于测试和与 MCP 服务器交互
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def pretty_print_json(obj):
|
||||
"""美化打印 JSON 对象"""
|
||||
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
||||
|
||||
def run_interactive_client():
|
||||
"""运行交互式客户端"""
|
||||
|
||||
script_dir = Path(__file__).parent
|
||||
binary_path = script_dir / "mcp-server.exe"
|
||||
|
||||
if not binary_path.exists():
|
||||
print(f"❌ Error: Binary not found at {binary_path}")
|
||||
print("Please build the project first:")
|
||||
print(" cd e:\\Demos\\DemoOwns\\Go\\yunzer_go\\server\\mcp-server")
|
||||
print(" go build -o mcp-server.exe main.go")
|
||||
return
|
||||
|
||||
print("🚀 Starting MySQL MCP Server...")
|
||||
|
||||
# 启动进程
|
||||
process = subprocess.Popen(
|
||||
[str(binary_path)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
print("✅ Server started. Type 'help' for commands.\n")
|
||||
|
||||
request_id = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
user_input = input(">>> ").strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input.lower() == "help":
|
||||
print("""
|
||||
Available commands:
|
||||
init - Initialize server
|
||||
tables - List all tables
|
||||
schema <table> - Get table schema
|
||||
query <sql> - Execute SELECT query
|
||||
exec <sql> - Execute INSERT/UPDATE/DELETE
|
||||
json <json_string> - Send raw JSON-RPC request
|
||||
help - Show this help
|
||||
exit / quit - Exit the client
|
||||
|
||||
Examples:
|
||||
> tables
|
||||
> schema users
|
||||
> query SELECT * FROM users LIMIT 5
|
||||
> exec INSERT INTO users (name, email) VALUES ('John', 'john@example.com')
|
||||
> json {"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"SELECT COUNT(*) as count FROM users"}}
|
||||
""")
|
||||
continue
|
||||
|
||||
if user_input.lower() in ["exit", "quit"]:
|
||||
break
|
||||
|
||||
request_id += 1
|
||||
|
||||
# 解析命令
|
||||
if user_input.lower() == "init":
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "initialize",
|
||||
"params": {}
|
||||
}
|
||||
|
||||
elif user_input.lower() == "tables":
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "get_tables",
|
||||
"params": {}
|
||||
}
|
||||
|
||||
elif user_input.lower().startswith("schema "):
|
||||
table = user_input[7:].strip()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "get_table_schema",
|
||||
"params": {"table": table}
|
||||
}
|
||||
|
||||
elif user_input.lower().startswith("query "):
|
||||
sql = user_input[6:].strip()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "query",
|
||||
"params": {"sql": sql, "args": []}
|
||||
}
|
||||
|
||||
elif user_input.lower().startswith("exec "):
|
||||
sql = user_input[5:].strip()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "execute",
|
||||
"params": {"sql": sql, "args": []}
|
||||
}
|
||||
|
||||
elif user_input.lower().startswith("json "):
|
||||
json_str = user_input[5:].strip()
|
||||
try:
|
||||
request = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Invalid JSON: {e}")
|
||||
continue
|
||||
|
||||
else:
|
||||
print("❌ Unknown command. Type 'help' for available commands.")
|
||||
continue
|
||||
|
||||
# 发送请求
|
||||
request_json = json.dumps(request)
|
||||
process.stdin.write(request_json + "\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# 读取响应
|
||||
response_str = process.stdout.readline()
|
||||
if response_str:
|
||||
try:
|
||||
response = json.loads(response_str)
|
||||
print("\n✅ Response:")
|
||||
pretty_print_json(response)
|
||||
print()
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Failed to parse response: {e}")
|
||||
print(f"Raw response: {response_str}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n^C Exiting...")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
finally:
|
||||
print("\n🛑 Stopping server...")
|
||||
process.terminate()
|
||||
process.wait()
|
||||
print("✓ Server stopped")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_interactive_client()
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"mysql": {
|
||||
"user": "gotest",
|
||||
"password": "2nZhRdMPCNZrdzsd",
|
||||
"host": "212.64.112.158",
|
||||
"port": 3388,
|
||||
"database": "gotest",
|
||||
"charset": "utf8mb4",
|
||||
"timeout": "10s",
|
||||
"readTimeout": "30s",
|
||||
"writeTimeout": "30s",
|
||||
"maxIdleConns": 10,
|
||||
"maxOpenConns": 100,
|
||||
"connMaxLifetime": "30m"
|
||||
},
|
||||
"server": {
|
||||
"logLevel": "info",
|
||||
"enableQueryLogging": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module mcp-server
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.7.0
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
@@ -0,0 +1,387 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// MCPRequest 表示 MCP 请求
|
||||
type MCPRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
// MCPResponse 表示 MCP 响应
|
||||
type MCPResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *MCPError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// MCPError 表示 MCP 错误
|
||||
type MCPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// QueryParams 表示查询参数
|
||||
type QueryParams struct {
|
||||
SQL string `json:"sql"`
|
||||
Args []interface{} `json:"args"`
|
||||
}
|
||||
|
||||
// ExecuteParams 表示执行参数
|
||||
type ExecuteParams struct {
|
||||
SQL string `json:"sql"`
|
||||
Args []interface{} `json:"args"`
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func main() {
|
||||
// 初始化数据库
|
||||
if err := initDatabase(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to initialize database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 启动 MCP 服务器
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析请求
|
||||
var req MCPRequest
|
||||
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||
sendError(nil, -32700, "Parse error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理请求
|
||||
handleRequest(&req)
|
||||
}
|
||||
}
|
||||
|
||||
func initDatabase() error {
|
||||
// 从环境变量或默认值读取配置
|
||||
user := getEnv("MYSQL_USER", "gotest")
|
||||
pass := getEnv("MYSQL_PASS", "2nZhRdMPCNZrdzsd")
|
||||
urls := getEnv("MYSQL_URLS", "212.64.112.158:3388")
|
||||
dbName := getEnv("MYSQL_DB", "gotest")
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s",
|
||||
user, pass, urls, dbName)
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
db.SetMaxIdleConns(10)
|
||||
db.SetMaxOpenConns(100)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Database connected successfully\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRequest(req *MCPRequest) {
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
handleInitialize(req)
|
||||
case "query":
|
||||
handleQuery(req)
|
||||
case "execute":
|
||||
handleExecute(req)
|
||||
case "get_tables":
|
||||
handleGetTables(req)
|
||||
case "get_table_schema":
|
||||
handleGetTableSchema(req)
|
||||
default:
|
||||
sendError(req.ID, -32601, "Method not found", fmt.Sprintf("Unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleInitialize(req *MCPRequest) {
|
||||
result := map[string]interface{}{
|
||||
"protocolVersion": "1.0",
|
||||
"capabilities": map[string]interface{}{
|
||||
"tools": []map[string]interface{}{
|
||||
{
|
||||
"name": "query",
|
||||
"description": "Execute a SELECT query and return results",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"sql": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "SQL SELECT query",
|
||||
},
|
||||
"args": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "Query parameters (optional)",
|
||||
},
|
||||
},
|
||||
"required": []string{"sql"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "execute",
|
||||
"description": "Execute an INSERT, UPDATE, or DELETE query",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"sql": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "SQL INSERT/UPDATE/DELETE query",
|
||||
},
|
||||
"args": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "Query parameters (optional)",
|
||||
},
|
||||
},
|
||||
"required": []string{"sql"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_tables",
|
||||
"description": "Get all table names in the database",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_table_schema",
|
||||
"description": "Get the schema of a specific table",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"table": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Table name",
|
||||
},
|
||||
},
|
||||
"required": []string{"table"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"serverInfo": map[string]interface{}{
|
||||
"name": "MySQL MCP Server",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
}
|
||||
sendResponse(req.ID, result)
|
||||
}
|
||||
|
||||
func handleQuery(req *MCPRequest) {
|
||||
var params QueryParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
sendError(req.ID, -32602, "Invalid params", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if params.SQL == "" {
|
||||
sendError(req.ID, -32602, "Invalid params", "SQL query is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保是 SELECT 查询
|
||||
if !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(params.SQL)), "SELECT") {
|
||||
sendError(req.ID, -32602, "Invalid query", "Only SELECT queries are allowed")
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(params.SQL, params.Args...)
|
||||
if err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 获取列名
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
values := make([]interface{}, len(columns))
|
||||
valuePtrs := make([]interface{}, len(columns))
|
||||
for i := range columns {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
b, ok := val.([]byte)
|
||||
if ok {
|
||||
entry[col] = string(b)
|
||||
} else {
|
||||
entry[col] = val
|
||||
}
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sendResponse(req.ID, map[string]interface{}{
|
||||
"rows": results,
|
||||
"count": len(results),
|
||||
})
|
||||
}
|
||||
|
||||
func handleExecute(req *MCPRequest) {
|
||||
var params ExecuteParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
sendError(req.ID, -32602, "Invalid params", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if params.SQL == "" {
|
||||
sendError(req.ID, -32602, "Invalid params", "SQL query is required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := db.Exec(params.SQL, params.Args...)
|
||||
if err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
lastID, _ := result.LastInsertId()
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
|
||||
sendResponse(req.ID, map[string]interface{}{
|
||||
"lastInsertId": lastID,
|
||||
"rowsAffected": rowsAffected,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetTables(req *MCPRequest) {
|
||||
rows, err := db.Query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE()")
|
||||
if err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
tables = append(tables, tableName)
|
||||
}
|
||||
|
||||
sendResponse(req.ID, map[string]interface{}{
|
||||
"tables": tables,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetTableSchema(req *MCPRequest) {
|
||||
var params struct {
|
||||
Table string `json:"table"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
sendError(req.ID, -32602, "Invalid params", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(fmt.Sprintf("DESCRIBE %s", params.Table))
|
||||
if err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var schema []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var field, typeStr, null, key, defaultVal, extra string
|
||||
if err := rows.Scan(&field, &typeStr, &null, &key, &defaultVal, &extra); err != nil {
|
||||
sendError(req.ID, -32603, "Database error", err.Error())
|
||||
return
|
||||
}
|
||||
schema = append(schema, map[string]interface{}{
|
||||
"field": field,
|
||||
"type": typeStr,
|
||||
"null": null,
|
||||
"key": key,
|
||||
"default": defaultVal,
|
||||
"extra": extra,
|
||||
})
|
||||
}
|
||||
|
||||
sendResponse(req.ID, schema)
|
||||
}
|
||||
|
||||
func sendResponse(id interface{}, result interface{}) {
|
||||
response := MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
}
|
||||
data, _ := json.Marshal(response)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
func sendError(id interface{}, code int, message string, data string) {
|
||||
response := MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &MCPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
jsonData, _ := json.Marshal(response)
|
||||
fmt.Println(string(jsonData))
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM MySQL MCP Server 启动脚本
|
||||
|
||||
REM 设置环境变量(可选,从 app.conf 读取)
|
||||
set MYSQL_USER=gotest
|
||||
set MYSQL_PASS=2nZhRdMPCNZrdzsd
|
||||
set MYSQL_URLS=212.64.112.158:3388
|
||||
set MYSQL_DB=gotest
|
||||
|
||||
REM 编译
|
||||
echo Building MCP Server...
|
||||
go build -o mcp-server.exe main.go
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build successful! Starting MCP Server...
|
||||
echo.
|
||||
|
||||
REM 启动服务器
|
||||
mcp-server.exe
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MySQL MCP Server 启动脚本
|
||||
|
||||
# 设置环境变量(可选)
|
||||
export MYSQL_USER=gotest
|
||||
export MYSQL_PASS=2nZhRdMPCNZrdzsd
|
||||
export MYSQL_URLS=212.64.112.158:3388
|
||||
export MYSQL_DB=gotest
|
||||
|
||||
# 编译
|
||||
echo "Building MCP Server..."
|
||||
go build -o mcp-server main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful! Starting MCP Server..."
|
||||
echo ""
|
||||
|
||||
# 启动服务器
|
||||
./mcp-server
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MySQL MCP Server 测试脚本
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
class MCPClient:
|
||||
"""MCP 客户端"""
|
||||
|
||||
def __init__(self, binary_path):
|
||||
"""初始化客户端"""
|
||||
self.binary_path = binary_path
|
||||
self.process = None
|
||||
self.request_id = 0
|
||||
|
||||
def start(self):
|
||||
"""启动 MCP 服务器"""
|
||||
print("Starting MCP Server...")
|
||||
self.process = subprocess.Popen(
|
||||
[self.binary_path],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
time.sleep(1) # 等待服务器启动
|
||||
print("✓ MCP Server started")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
print("✓ MCP Server stopped")
|
||||
|
||||
def send_request(self, method, params=None):
|
||||
"""发送请求"""
|
||||
self.request_id += 1
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": self.request_id,
|
||||
"method": method,
|
||||
"params": params or {}
|
||||
}
|
||||
|
||||
json_str = json.dumps(request)
|
||||
print(f"\n→ Sending: {method}")
|
||||
print(f" Request: {json_str}")
|
||||
|
||||
self.process.stdin.write(json_str + "\n")
|
||||
self.process.stdin.flush()
|
||||
|
||||
# 读取响应
|
||||
response_str = self.process.stdout.readline()
|
||||
print(f" Response: {response_str.strip()}")
|
||||
|
||||
try:
|
||||
response = json.loads(response_str)
|
||||
return response
|
||||
except json.JSONDecodeError as e:
|
||||
print(f" Error parsing response: {e}")
|
||||
return None
|
||||
|
||||
def test_mcp_server():
|
||||
"""测试 MCP 服务器"""
|
||||
|
||||
# 获取二进制文件路径
|
||||
script_dir = Path(__file__).parent
|
||||
binary_path = script_dir / "mcp-server.exe"
|
||||
|
||||
if not binary_path.exists():
|
||||
print(f"Error: Binary not found at {binary_path}")
|
||||
print("Please build the project first: go build -o mcp-server.exe main.go")
|
||||
return False
|
||||
|
||||
client = MCPClient(str(binary_path))
|
||||
|
||||
try:
|
||||
# 启动服务器
|
||||
client.start()
|
||||
|
||||
# 测试 1: 初始化
|
||||
print("\n" + "="*50)
|
||||
print("Test 1: Initialize")
|
||||
print("="*50)
|
||||
response = client.send_request("initialize")
|
||||
if response and "result" in response:
|
||||
print("✓ Initialize successful")
|
||||
else:
|
||||
print("✗ Initialize failed")
|
||||
return False
|
||||
|
||||
# 测试 2: 获取表列表
|
||||
print("\n" + "="*50)
|
||||
print("Test 2: Get Tables")
|
||||
print("="*50)
|
||||
response = client.send_request("get_tables")
|
||||
if response and "result" in response:
|
||||
tables = response["result"].get("tables", [])
|
||||
print(f"✓ Get tables successful, found {len(tables)} tables")
|
||||
if tables:
|
||||
print(f" Tables: {', '.join(tables[:5])}")
|
||||
else:
|
||||
print("✗ Get tables failed")
|
||||
return False
|
||||
|
||||
# 测试 3: 查询数据
|
||||
print("\n" + "="*50)
|
||||
print("Test 3: Query Data")
|
||||
print("="*50)
|
||||
response = client.send_request("query", {
|
||||
"sql": "SELECT * FROM users LIMIT 5",
|
||||
"args": []
|
||||
})
|
||||
if response and "result" in response:
|
||||
count = response["result"].get("count", 0)
|
||||
print(f"✓ Query successful, returned {count} rows")
|
||||
if count > 0:
|
||||
print(f" Sample row: {response['result']['rows'][0]}")
|
||||
else:
|
||||
print("✗ Query failed")
|
||||
if "error" in response:
|
||||
print(f" Error: {response['error']['message']}")
|
||||
|
||||
# 测试 4: 获取表结构
|
||||
print("\n" + "="*50)
|
||||
print("Test 4: Get Table Schema")
|
||||
print("="*50)
|
||||
response = client.send_request("get_table_schema", {
|
||||
"table": "users"
|
||||
})
|
||||
if response and "result" in response:
|
||||
schema = response["result"]
|
||||
print(f"✓ Get schema successful, found {len(schema)} columns")
|
||||
for col in schema[:3]:
|
||||
print(f" - {col['field']}: {col['type']}")
|
||||
else:
|
||||
print("✗ Get schema failed")
|
||||
|
||||
# 测试 5: 参数化查询
|
||||
print("\n" + "="*50)
|
||||
print("Test 5: Parameterized Query")
|
||||
print("="*50)
|
||||
response = client.send_request("query", {
|
||||
"sql": "SELECT * FROM users WHERE id = ?",
|
||||
"args": [1]
|
||||
})
|
||||
if response and "result" in response:
|
||||
count = response["result"].get("count", 0)
|
||||
print(f"✓ Parameterized query successful, returned {count} rows")
|
||||
else:
|
||||
print("✗ Parameterized query failed")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("All tests completed!")
|
||||
print("="*50)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
finally:
|
||||
client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_mcp_server()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"comment": "VS Code MCP 配置示例 - 将此配置添加到你的 VS Code settings.json",
|
||||
"modelContextProtocol": {
|
||||
"servers": {
|
||||
"mysql": {
|
||||
"command": "e:\\Demos\\DemoOwns\\Go\\yunzer_go\\server\\mcp-server\\mcp-server.exe",
|
||||
"env": {
|
||||
"MYSQL_USER": "gotest",
|
||||
"MYSQL_PASS": "2nZhRdMPCNZrdzsd",
|
||||
"MYSQL_URLS": "212.64.112.158:3388",
|
||||
"MYSQL_DB": "gotest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"server/models"
|
||||
@@ -12,15 +13,22 @@ import (
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有接口的调用记录
|
||||
func OperationLogMiddleware(ctx *context.Context) {
|
||||
// 记录所有重要操作,包括修改类(POST/PUT/PATCH/DELETE)和读取类(GET)用于统计账户访问功能
|
||||
// 跳过静态资源和内部路由
|
||||
url := ctx.Input.URL()
|
||||
if shouldSkipLogging(url) {
|
||||
return
|
||||
}
|
||||
|
||||
method := ctx.Input.Method()
|
||||
|
||||
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
|
||||
userId := 0
|
||||
tenantId := 0
|
||||
username := ""
|
||||
userType := "" // 用户类型:user(平台用户) 或 employee(租户员工)
|
||||
|
||||
if v := ctx.Input.GetData("userId"); v != nil {
|
||||
if id, ok := v.(int); ok {
|
||||
userId = id
|
||||
@@ -36,112 +44,90 @@ func OperationLogMiddleware(ctx *context.Context) {
|
||||
username = s
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法获取用户ID,继续记录为匿名访问(userId=0),以便统计未登录或授权失败的访问
|
||||
if userId == 0 {
|
||||
// debug: 输出一些上下文信息,帮助定位为何未能获取 userId
|
||||
fmt.Printf("OperationLogMiddleware: anonymous request %s %s, Authorization header length=%d\n", method, ctx.Input.URL(), len(ctx.Input.Header("Authorization")))
|
||||
// 确保 username 有值,便于区分
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
if v := ctx.Input.GetData("userType"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
userType = s
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息补全
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
}
|
||||
|
||||
// 读取请求体(对于有请求体的方法)
|
||||
var requestBody string
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err == nil {
|
||||
if err == nil && len(body) > 0 {
|
||||
requestBody = string(body)
|
||||
// 重置请求体,使其可以被后续处理
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody))
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
ipAddress := ctx.Input.IP()
|
||||
userAgent := ctx.Input.Header("User-Agent")
|
||||
queryString := ctx.Request.URL.RawQuery
|
||||
|
||||
// 使用延迟函数来记录操作
|
||||
defer func() {
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 解析操作类型
|
||||
operation := parseOperationType(method, ctx.Input.URL())
|
||||
resourceType := parseResourceType(ctx.Input.URL())
|
||||
resourceId := parseResourceId(ctx.Input.URL())
|
||||
module := parseModule(ctx.Input.URL())
|
||||
// 解析操作相关信息
|
||||
operation := parseOperationType(method, url)
|
||||
module := parseModule(url)
|
||||
resourceType := parseResourceType(url)
|
||||
resourceId := parseResourceId(url)
|
||||
|
||||
// 如果是读取/访问行为,写入访问日志(sys_access_log),否则写入操作日志(sys_operation_log)
|
||||
if operation == "READ" {
|
||||
access := &models.AccessLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: module,
|
||||
ResourceType: resourceType,
|
||||
ResourceId: &resourceId,
|
||||
RequestUrl: ctx.Input.URL(),
|
||||
IpAddress: ctx.Input.IP(),
|
||||
UserAgent: ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: method,
|
||||
Duration: int(duration.Milliseconds()),
|
||||
}
|
||||
|
||||
// 在 QueryString 中记录查询参数和匿名标识
|
||||
qs := ctx.Request.URL.RawQuery
|
||||
if qs != "" {
|
||||
access.QueryString = qs
|
||||
}
|
||||
if userId == 0 {
|
||||
// 将匿名标识拼入 QueryString 以便查询(也可改为独立字段)
|
||||
if access.QueryString != "" {
|
||||
access.QueryString = "anonymous=true; " + access.QueryString
|
||||
} else {
|
||||
access.QueryString = "anonymous=true"
|
||||
}
|
||||
}
|
||||
|
||||
if err := services.AddAccessLog(access); err != nil {
|
||||
fmt.Printf("Failed to save access log: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建操作日志
|
||||
// 为所有接口都记录日志
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: module,
|
||||
ResourceType: resourceType,
|
||||
ResourceId: &resourceId,
|
||||
Operation: operation,
|
||||
IpAddress: ctx.Input.IP(),
|
||||
UserAgent: ctx.Input.Header("User-Agent"),
|
||||
IpAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
RequestMethod: method,
|
||||
RequestUrl: ctx.Input.URL(),
|
||||
Status: 1, // 默认成功,实际应该根据响应状态码更新
|
||||
RequestUrl: url,
|
||||
Status: 1, // 默认成功
|
||||
Duration: int(duration.Milliseconds()),
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 如果是写操作,保存请求体作为新值;对于读取操作可以在Description里记录query
|
||||
if requestBody != "" {
|
||||
log.NewValue = requestBody
|
||||
} else if method == "GET" {
|
||||
// 把查询字符串放到描述里,便于分析访问参数
|
||||
qs := ctx.Request.URL.RawQuery
|
||||
if qs != "" {
|
||||
log.Description = "query=" + qs
|
||||
}
|
||||
// 设置资源ID
|
||||
if resourceId > 0 {
|
||||
log.ResourceId = &resourceId
|
||||
}
|
||||
|
||||
// 标记匿名访问信息(当 userId==0)
|
||||
if userId == 0 {
|
||||
if log.Description != "" {
|
||||
log.Description = "anonymous=true; " + log.Description
|
||||
} else {
|
||||
log.Description = "anonymous=true"
|
||||
// 记录请求信息到Description
|
||||
var description strings.Builder
|
||||
if requestBody != "" {
|
||||
description.WriteString("Request: " + truncateString(requestBody, 500))
|
||||
}
|
||||
if queryString != "" {
|
||||
if description.Len() > 0 {
|
||||
description.WriteString(" | ")
|
||||
}
|
||||
description.WriteString("Query: " + queryString)
|
||||
}
|
||||
|
||||
log.Description = description.String()
|
||||
|
||||
// 如果有请求体,作为NewValue保存
|
||||
if requestBody != "" {
|
||||
log.NewValue = requestBody
|
||||
}
|
||||
|
||||
// 添加用户类型信息到Description
|
||||
if userType != "" {
|
||||
if log.Description != "" {
|
||||
log.Description += " | "
|
||||
}
|
||||
log.Description += "UserType: " + userType
|
||||
}
|
||||
|
||||
// 调用服务层保存日志
|
||||
@@ -207,3 +193,30 @@ func parseModule(url string) string {
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// shouldSkipLogging 判断是否需要跳过日志记录
|
||||
func shouldSkipLogging(url string) bool {
|
||||
// 跳过静态资源、健康检查等
|
||||
skipPatterns := []string{
|
||||
"/static/",
|
||||
"/uploads/",
|
||||
"/favicon.ico",
|
||||
"/health",
|
||||
"/ping",
|
||||
}
|
||||
|
||||
for _, pattern := range skipPatterns {
|
||||
if strings.HasPrefix(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// truncateString 截断字符串到指定长度
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Task 任务模型(对应表 yz_tenant_tasks)
|
||||
type Task struct {
|
||||
Id int `orm:"auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
TaskNo string `orm:"column(task_no);size(64)" json:"task_no"`
|
||||
TaskName string `orm:"column(task_name);size(255)" json:"task_name"`
|
||||
TaskDesc string `orm:"column(task_desc);type(text);null" json:"task_desc"`
|
||||
TaskType string `orm:"column(task_type);size(32);null" json:"task_type"`
|
||||
BusinessTag string `orm:"column(business_tag);size(64);null" json:"business_tag"`
|
||||
ParentTaskId int `orm:"column(parent_task_id);null" json:"parent_task_id"`
|
||||
ProjectId int `orm:"column(project_id);null" json:"project_id"`
|
||||
RelatedId int `orm:"column(related_id);null" json:"related_id"`
|
||||
RelatedType string `orm:"column(related_type);size(32);null" json:"related_type"`
|
||||
TeamEmployeeIds string `orm:"column(team_employee_ids);size(512);null" json:"team_employee_ids"`
|
||||
CreatorId int `orm:"column(creator_id)" json:"creator_id"`
|
||||
CreatorName string `orm:"column(creator_name);size(64)" json:"creator_name"`
|
||||
PrincipalId int `orm:"column(principal_id)" json:"principal_id"`
|
||||
PrincipalName string `orm:"column(principal_name);size(64)" json:"principal_name"`
|
||||
ParticipantIds string `orm:"column(participant_ids);size(512);null" json:"participant_ids"`
|
||||
ParticipantNames string `orm:"column(participant_names);size(512);null" json:"participant_names"`
|
||||
CcIds string `orm:"column(cc_ids);size(512);null" json:"cc_ids"`
|
||||
CcNames string `orm:"column(cc_names);size(512);null" json:"cc_names"`
|
||||
PlanStartTime *time.Time `orm:"column(plan_start_time);null;type(datetime)" json:"plan_start_time"`
|
||||
PlanEndTime time.Time `orm:"column(plan_end_time);type(datetime)" json:"plan_end_time"`
|
||||
ActualStartTime *time.Time `orm:"column(actual_start_time);null;type(datetime)" json:"actual_start_time"`
|
||||
ActualEndTime *time.Time `orm:"column(actual_end_time);null;type(datetime)" json:"actual_end_time"`
|
||||
EstimatedHours float64 `orm:"column(estimated_hours);null;digits(10);decimals(2)" json:"estimated_hours"`
|
||||
ActualHours float64 `orm:"column(actual_hours);null;digits(10);decimals(2)" json:"actual_hours"`
|
||||
TaskStatus string `orm:"column(task_status);size(32)" json:"task_status"`
|
||||
Priority string `orm:"column(priority);size(16)" json:"priority"`
|
||||
Progress int8 `orm:"column(progress)" json:"progress"`
|
||||
NeedApproval int8 `orm:"column(need_approval)" json:"need_approval"`
|
||||
ApprovalId int `orm:"column(approval_id);null" json:"approval_id"`
|
||||
DelayApproved int8 `orm:"column(delay_approved)" json:"delay_approved"`
|
||||
OldPlanEndTime *time.Time `orm:"column(old_plan_end_time);null;type(datetime)" json:"old_plan_end_time"`
|
||||
RepeatType string `orm:"column(repeat_type);size(16);null" json:"repeat_type"`
|
||||
RepeatCycle int `orm:"column(repeat_cycle);null" json:"repeat_cycle"`
|
||||
RepeatEndTime *time.Time `orm:"column(repeat_end_time);null;type(datetime)" json:"repeat_end_time"`
|
||||
AttachmentIds string `orm:"column(attachment_ids);size(1024);null" json:"attachment_ids"`
|
||||
Remark string `orm:"column(remark);size(512);null" json:"remark"`
|
||||
IsArchived int8 `orm:"column(is_archived)" json:"is_archived"`
|
||||
ArchiveTime *time.Time `orm:"column(archive_time);null;type(datetime)" json:"archive_time"`
|
||||
CreatedTime time.Time `orm:"column(created_time);type(datetime);auto_now_add" json:"created_time"`
|
||||
UpdatedTime time.Time `orm:"column(updated_time);type(datetime);auto_now" json:"updated_time"`
|
||||
DeletedTime *time.Time `orm:"column(deleted_time);null;type(datetime)" json:"deleted_time"`
|
||||
OperatorId int `orm:"column(operator_id);null" json:"operator_id"`
|
||||
OperatorName string `orm:"column(operator_name);size(64);null" json:"operator_name"`
|
||||
}
|
||||
|
||||
func (t *Task) TableName() string {
|
||||
return "yz_tenant_tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Task))
|
||||
}
|
||||
|
||||
// -------- 数据访问函数 ---------
|
||||
|
||||
// ListTasks 分页查询任务
|
||||
func ListTasks(tenantId int, keyword, status, priority string, page, pageSize int) (tasks []*Task, total int64, err error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable(new(Task)).Filter("tenant_id", tenantId).Filter("deleted_time__isnull", true)
|
||||
|
||||
if keyword != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond1 := cond.Or("task_name__icontains", keyword).Or("task_no__icontains", keyword).Or("principal_name__icontains", keyword)
|
||||
qs = qs.SetCond(cond1)
|
||||
}
|
||||
if status != "" {
|
||||
qs = qs.Filter("task_status", status)
|
||||
}
|
||||
if priority != "" {
|
||||
qs = qs.Filter("priority", priority)
|
||||
}
|
||||
|
||||
total, err = qs.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-id").Limit(pageSize, offset).All(&tasks)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTaskById 获取单个任务
|
||||
func GetTaskById(id int) (*Task, error) {
|
||||
o := orm.NewOrm()
|
||||
t := Task{Id: id}
|
||||
err := o.Read(&t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// CreateTask 新建任务
|
||||
func CreateTask(t *Task) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Insert(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTask 更新任务
|
||||
func UpdateTask(t *Task, cols ...string) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Update(t, cols...)
|
||||
return err
|
||||
}
|
||||
@@ -29,6 +29,17 @@ type User struct {
|
||||
LastLoginIp string `orm:"column(last_login_ip);null;size(50)" json:"last_login_ip"`
|
||||
}
|
||||
|
||||
// GetUserById 根据ID获取用户信息
|
||||
func GetUserById(id int) (*User, error) {
|
||||
o := orm.NewOrm()
|
||||
user := &User{Id: id}
|
||||
err := o.Read(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// TableName 设置表名,默认为yz_users
|
||||
func (u *User) TableName() string {
|
||||
return "yz_users"
|
||||
@@ -47,6 +58,7 @@ func Init(version string) {
|
||||
orm.RegisterModel(new(DictType))
|
||||
orm.RegisterModel(new(DictItem))
|
||||
orm.RegisterModel(new(OperationLog))
|
||||
orm.RegisterModel(new(AccessLog))
|
||||
|
||||
ormConfig, err := beego.AppConfig.String("orm")
|
||||
if err != nil {
|
||||
@@ -90,5 +102,20 @@ func Init(version string) {
|
||||
fmt.Println("数据库连接成功!")
|
||||
fmt.Printf("当前项目版本: %s\n", version)
|
||||
fmt.Println("数据库连接池配置: MaxIdleConns=10, MaxOpenConns=100, ConnMaxLifetime=1h")
|
||||
|
||||
// 自动创建或更新表结构
|
||||
o := orm.NewOrm()
|
||||
_, err = o.Raw("SET FOREIGN_KEY_CHECKS=0").Exec()
|
||||
if err != nil {
|
||||
fmt.Println("关闭外键检查失败:", err)
|
||||
}
|
||||
if err := orm.RunSyncdb("default", false, true); err != nil {
|
||||
fmt.Println("数据库表同步失败:", err)
|
||||
}
|
||||
_, err = o.Raw("SET FOREIGN_KEY_CHECKS=1").Exec()
|
||||
if err != nil {
|
||||
fmt.Println("启用外键检查失败:", err)
|
||||
}
|
||||
fmt.Println("数据库表自动同步完成")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,10 @@ func init() {
|
||||
// OA基础数据合并接口(一次性获取部门、职位、角色)
|
||||
beego.Router("/api/oa/base-data/:tenantId", &controllers.OAController{}, "get:GetOABaseData")
|
||||
|
||||
// OA任务管理路由
|
||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||
|
||||
// 权限管理路由
|
||||
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
|
||||
beego.Router("/api/permissions/role/:roleId", &controllers.PermissionController{}, "get:GetRolePermissions")
|
||||
@@ -323,6 +327,7 @@ func init() {
|
||||
// 仪表盘路由
|
||||
beego.Router("/api/dashboard/platform-stats", &controllers.DashboardController{}, "get:GetPlatformStats")
|
||||
beego.Router("/api/dashboard/tenant-stats", &controllers.DashboardController{}, "get:GetTenantStats")
|
||||
beego.Router("/api/dashboard/user-activity-logs", &controllers.DashboardController{}, "get:GetUserActivityLogs")
|
||||
|
||||
// 字典管理路由
|
||||
beego.Router("/api/dict/types", &controllers.DictController{}, "get:GetDictTypes;post:AddDictType")
|
||||
@@ -345,4 +350,10 @@ func init() {
|
||||
beego.Router("/api/operation-logs/tenant/stats", &controllers.OperationLogController{}, "get:GetTenantStats")
|
||||
beego.Router("/api/operation-logs/clear", &controllers.OperationLogController{}, "post:ClearOldLogs")
|
||||
|
||||
// 访问日志路由 - 统一到操作日志控制器
|
||||
beego.Router("/api/access-logs", &controllers.OperationLogController{}, "get:GetAccessLogs")
|
||||
beego.Router("/api/access-logs/:id", &controllers.OperationLogController{}, "get:GetAccessLogById")
|
||||
beego.Router("/api/access-logs/user/stats", &controllers.OperationLogController{}, "get:GetUserAccessStats")
|
||||
beego.Router("/api/access-logs/clear", &controllers.OperationLogController{}, "post:ClearOldAccessLogs")
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -41,7 +41,7 @@ func GetModuleName(module string) string {
|
||||
|
||||
// 如果菜单表中找不到,使用默认映射
|
||||
defaultMap := map[string]string{
|
||||
"auth": "认证",
|
||||
"auth": "登录模块",
|
||||
"dict": "字典管理",
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
@@ -142,7 +142,7 @@ func GetModuleNames(modules []string) (map[string]string, error) {
|
||||
|
||||
// 3. 对于仍然没有匹配的,使用默认映射
|
||||
defaultMap := map[string]string{
|
||||
"auth": "认证",
|
||||
"auth": "登录模块",
|
||||
"dict": "字典管理",
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
@@ -367,3 +367,139 @@ func DeleteOldLogs(keepDays int) (int64, error) {
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
// GetAccessLogs 获取访问日志列表
|
||||
func GetAccessLogs(tenantId int, userId int, module string, resourceType string, startTime *time.Time, endTime *time.Time, pageNum int, pageSize int) ([]*models.AccessLog, int64, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable("sys_access_log")
|
||||
|
||||
// 租户过滤
|
||||
if tenantId > 0 {
|
||||
qs = qs.Filter("tenant_id", tenantId)
|
||||
}
|
||||
|
||||
// 用户过滤
|
||||
if userId > 0 {
|
||||
qs = qs.Filter("user_id", userId)
|
||||
}
|
||||
|
||||
// 模块过滤
|
||||
if module != "" {
|
||||
qs = qs.Filter("module", module)
|
||||
}
|
||||
|
||||
// 资源类型过滤
|
||||
if resourceType != "" {
|
||||
qs = qs.Filter("resource_type", resourceType)
|
||||
}
|
||||
|
||||
// 时间范围过滤
|
||||
if startTime != nil {
|
||||
qs = qs.Filter("create_time__gte", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
qs = qs.Filter("create_time__lte", endTime)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询日志总数失败: %v", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var logs []*models.AccessLog
|
||||
offset := (pageNum - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time").Offset(offset).Limit(pageSize).All(&logs)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询访问日志失败: %v", err)
|
||||
}
|
||||
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
// GetAccessLogById 根据ID获取访问日志
|
||||
func GetAccessLogById(id int64) (*models.AccessLog, error) {
|
||||
o := orm.NewOrm()
|
||||
log := &models.AccessLog{Id: id}
|
||||
err := o.Read(log)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("日志不存在: %v", err)
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// GetUserAccessStats 获取用户访问统计
|
||||
func GetUserAccessStats(tenantId int, userId int, days int) (map[string]interface{}, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
startTime := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
// 获取总访问数
|
||||
var totalCount int64
|
||||
err := o.Raw(
|
||||
"SELECT COUNT(*) FROM sys_access_log WHERE tenant_id = ? AND user_id = ? AND create_time >= ?",
|
||||
tenantId, userId, startTime,
|
||||
).QueryRow(&totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取按模块分组的访问数
|
||||
type ModuleCount struct {
|
||||
Module string
|
||||
Count int
|
||||
}
|
||||
var moduleCounts []ModuleCount
|
||||
_, err = o.Raw(
|
||||
"SELECT module, COUNT(*) as count FROM sys_access_log WHERE tenant_id = ? AND user_id = ? AND create_time >= ? GROUP BY module",
|
||||
tenantId, userId, startTime,
|
||||
).QueryRows(&moduleCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取按状态分组的访问数
|
||||
type StatusCount struct {
|
||||
Status int
|
||||
Count int
|
||||
}
|
||||
var statusCounts []StatusCount
|
||||
_, err = o.Raw(
|
||||
"SELECT status, COUNT(*) as count FROM sys_access_log WHERE tenant_id = ? AND user_id = ? AND create_time >= ? GROUP BY status",
|
||||
tenantId, userId, startTime,
|
||||
).QueryRows(&statusCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
stats := map[string]interface{}{
|
||||
"total_access": totalCount,
|
||||
"module_stats": moduleCounts,
|
||||
"status_stats": statusCounts,
|
||||
"period_days": days,
|
||||
"from_time": startTime,
|
||||
"to_time": time.Now(),
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// ClearOldAccessLogs 清除旧访问日志(保留指定天数)
|
||||
func ClearOldAccessLogs(keepDays int) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
cutoffTime := time.Now().AddDate(0, 0, -keepDays)
|
||||
|
||||
result, err := o.Raw("DELETE FROM sys_access_log WHERE create_time < ?", cutoffTime).Exec()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("删除旧访问日志失败: %v", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取受影响行数失败: %v", err)
|
||||
}
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ListOATasks 服务层:分页查询任务
|
||||
func ListOATasks(tenantId int, keyword, status, priority string, page, pageSize int) (tasks []*models.Task, total int64, err error) {
|
||||
return models.ListTasks(tenantId, keyword, status, priority, page, pageSize)
|
||||
}
|
||||
|
||||
// GetOATaskById 服务层:获取任务详情
|
||||
func GetOATaskById(id int) (*models.Task, error) {
|
||||
return models.GetTaskById(id)
|
||||
}
|
||||
|
||||
// CreateOATask 服务层:创建任务
|
||||
func CreateOATask(t *models.Task, tenantId int, username string) error {
|
||||
// 填充租户
|
||||
if t.TenantId == 0 && tenantId > 0 {
|
||||
t.TenantId = tenantId
|
||||
}
|
||||
// 默认状态与优先级
|
||||
if t.TaskStatus == "" {
|
||||
t.TaskStatus = ""
|
||||
}
|
||||
if t.Priority == "" {
|
||||
t.Priority = ""
|
||||
}
|
||||
// 任务编号
|
||||
if t.TaskNo == "" {
|
||||
t.TaskNo = genTaskNo(t.TenantId)
|
||||
}
|
||||
// 创建人与操作人
|
||||
if t.CreatorName == "" && username != "" {
|
||||
t.CreatorName = username
|
||||
}
|
||||
if t.OperatorName == "" && username != "" {
|
||||
t.OperatorName = username
|
||||
}
|
||||
return models.CreateTask(t)
|
||||
}
|
||||
|
||||
// UpdateOATask 服务层:更新任务
|
||||
func UpdateOATask(t *models.Task, username string, cols ...string) error {
|
||||
if username != "" {
|
||||
t.OperatorName = username
|
||||
}
|
||||
return models.UpdateTask(t, cols...)
|
||||
}
|
||||
|
||||
// DeleteOATask 服务层:软删除任务
|
||||
func DeleteOATask(id int, operatorName string, operatorId int) error {
|
||||
// 读取当前任务
|
||||
t, err := models.GetTaskById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
t.DeletedTime = &now
|
||||
t.OperatorName = operatorName
|
||||
t.OperatorId = operatorId
|
||||
return models.UpdateTask(t, "deleted_time", "operator_name", "operator_id")
|
||||
}
|
||||
|
||||
// 生成任务编号:TASK{tenantId}{YYYYMMDDHHMMSS}
|
||||
func genTaskNo(tenantId int) string {
|
||||
return fmt.Sprintf("TASK%d%s", tenantId, time.Now().Format("20060102150405"))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 创建访问日志表
|
||||
CREATE TABLE IF NOT EXISTS `sys_access_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
`user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`username` varchar(64) DEFAULT '' COMMENT '用户名',
|
||||
`ip_address` varchar(50) DEFAULT '' COMMENT 'IP地址',
|
||||
`module` varchar(64) DEFAULT '' COMMENT '模块',
|
||||
`action` varchar(128) DEFAULT '' COMMENT '操作',
|
||||
`resource_type` varchar(64) DEFAULT '' COMMENT '资源类型',
|
||||
`resource_id` varchar(255) DEFAULT '' COMMENT '资源ID',
|
||||
`request_method` varchar(10) DEFAULT '' COMMENT '请求方法',
|
||||
`request_url` varchar(255) DEFAULT '' COMMENT '请求URL',
|
||||
`user_agent` varchar(255) DEFAULT '' COMMENT 'User Agent',
|
||||
`status_code` int DEFAULT 200 COMMENT '状态码',
|
||||
`response_time` bigint DEFAULT 0 COMMENT '响应时间(毫秒)',
|
||||
`description` longtext COMMENT '日志描述',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`create_by` varchar(64) DEFAULT '' COMMENT '创建人',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`update_by` varchar(64) DEFAULT '' COMMENT '更新人',
|
||||
`delete_flag` tinyint DEFAULT 0 COMMENT '删除标记(0-正常,1-删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统访问日志表';
|
||||
Binary file not shown.
Reference in New Issue
Block a user