增加任务管理模块

This commit is contained in:
2025-11-12 17:32:03 +08:00
parent 12a0ff8afc
commit db16ee70de
54 changed files with 3638 additions and 672 deletions
+2 -2
View File
@@ -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",
+103
View File
@@ -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()
}
+191
View File
@@ -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(&params); err != nil {
// 如果ParseForm失败,尝试解析JSON
c.Ctx.Input.Bind(&params, "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()
}
+306
View File
@@ -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() {
// 获取租户IDJWT中间件写入)
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()
}
+75
View File
@@ -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系统任务表(多租户适配)';
+166
View File
@@ -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()
+20
View File
@@ -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
}
}
+5
View File
@@ -0,0 +1,5 @@
module mcp-server
go 1.17
require github.com/go-sql-driver/mysql v1.7.0
+2
View File
@@ -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=
+387
View File
@@ -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, &params); 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, &params); 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, &params); 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.
+23
View File
@@ -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
+24
View File
@@ -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
+174
View File
@@ -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"
}
}
}
}
}
+87 -74
View File
@@ -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] + "..."
}
+123
View File
@@ -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
}
+27
View File
@@ -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("数据库表自动同步完成")
}
}
+11
View File
@@ -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")
}
BIN
View File
Binary file not shown.
+138 -2
View File
@@ -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
}
+71
View File
@@ -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"))
}
+28
View File
@@ -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.