'init'
This commit is contained in:
34
sc-lktx-backend/internal/common/middleware/admin_jwt.go
Normal file
34
sc-lktx-backend/internal/common/middleware/admin_jwt.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
"com.sclktx/m/v2/internal/common/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AdminJWTAuth(secret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Unauthorized(c, "未提供认证令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var tokenStr string
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
tokenStr = authHeader[7:]
|
||||
} else {
|
||||
tokenStr = authHeader
|
||||
}
|
||||
claims, err := utils.ParseAdminToken(tokenStr, secret)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "管理员令牌无效或已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("admin_id", claims.AdminID)
|
||||
c.Set("admin_account", claims.Account)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
25
sc-lktx-backend/internal/common/middleware/cors.go
Normal file
25
sc-lktx-backend/internal/common/middleware/cors.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 中间件:Gin 的中间件是 func(*gin.Context),通过 c.Next() 控制执行链
|
||||
// 执行顺序:请求进入 → 中间件1(c.Next前) → 中间件2(c.Next前) → Handler → 中间件2(c.Next后) → 中间件1(c.Next后) → 响应
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 跨域中间件,允许 H5 调试时跨域访问
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
45
sc-lktx-backend/internal/common/middleware/jwt.go
Normal file
45
sc-lktx-backend/internal/common/middleware/jwt.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
"com.sclktx/m/v2/internal/common/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// JWTAuth JWT 认证中间件
|
||||
// 1. 从 Authorization header 提取 Bearer Token
|
||||
// 2. 解析验证 JWT(签名 + 过期时间)
|
||||
// 3. 将 user_id、role_code、role_codes 注入 gin.Context
|
||||
// 4. 后续 Handler 通过 utils.GetUserID(c) / utils.GetRoleCodes(c) 获取
|
||||
func JWTAuth(secret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Unauthorized(c, "未提供认证令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
response.Unauthorized(c, "认证格式错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := utils.ParseToken(parts[1], secret)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "认证令牌无效或已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 注入用户信息到 Context
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("role_code", claims.RoleCode)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
31
sc-lktx-backend/internal/common/middleware/logger.go
Normal file
31
sc-lktx-backend/internal/common/middleware/logger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Logger 请求日志中间件,记录每个请求的方法、路径、状态码、耗时
|
||||
// 在 c.Next() 前后分别记录开始和结束时间
|
||||
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
c.Next() // 执行后续中间件和 Handler
|
||||
|
||||
cost := time.Since(start)
|
||||
logger.Info("HTTP Request",
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", path),
|
||||
zap.String("query", query),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
zap.Duration("cost", cost),
|
||||
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
|
||||
)
|
||||
}
|
||||
}
|
||||
15
sc-lktx-backend/internal/common/model/base.go
Normal file
15
sc-lktx-backend/internal/common/model/base.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BaseModel 基础模型,所有表内嵌此结构,提供 ID、时间戳、软删除
|
||||
type BaseModel struct {
|
||||
ID uint `gorm:"primarykey;comment:主键ID" json:"id"`
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间(软删除)" json:"deleted_at,omitempty"`
|
||||
}
|
||||
332
sc-lktx-backend/internal/common/model/models.go
Normal file
332
sc-lktx-backend/internal/common/model/models.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
BaseModel
|
||||
Openid string `gorm:"uniqueIndex;size:100;not null;comment:微信OpenID" json:"openid"`
|
||||
Nickname string `gorm:"size:50;comment:微信昵称" json:"nickname"`
|
||||
RealName string `gorm:"size:50;comment:真实姓名" json:"real_name"`
|
||||
Phone string `gorm:"size:20;comment:手机号" json:"phone"`
|
||||
PhoneVerified bool `gorm:"default:false;comment:手机号是否已验证" json:"phone_verified"`
|
||||
AvatarURL string `gorm:"size:500;comment:头像URL" json:"avatar_url"`
|
||||
FaceImageURL string `gorm:"size:500;comment:人脸图片URL" json:"face_image_url"`
|
||||
Role string `gorm:"size:20;default:visitor;comment:角色(visitor/employee/guard)" json:"role"`
|
||||
Status int `gorm:"default:1;comment:用户状态(1正常 0禁用)" json:"status"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "users" }
|
||||
|
||||
// UserDepartment 员工-部门关联表
|
||||
type UserDepartment struct {
|
||||
ID uint `gorm:"primarykey;comment:主键ID" json:"id"`
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
UserID uint `gorm:"uniqueIndex;not null;comment:用户ID" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID;comment:关联用户信息" json:"user,omitempty"`
|
||||
DepartmentID uint `gorm:"not null;index;comment:部门ID" json:"department_id"`
|
||||
Department *Department `gorm:"foreignKey:DepartmentID;comment:关联部门信息" json:"department,omitempty"`
|
||||
DepartmentName string `gorm:"size:100;comment:部门名称(冗余)" json:"department_name"`
|
||||
Position string `gorm:"size:100;comment:职位" json:"position"`
|
||||
EmployeeType string `gorm:"size:30;default:employee;comment:员工类型" json:"employee_type"`
|
||||
EmployeeStatus int `gorm:"default:1;comment:员工状态(1在职 0离职)" json:"employee_status"`
|
||||
DingTalkUserID string `gorm:"column:ding_talk_user_id;size:100;comment:钉钉用户unionId(从通讯录导入)" json:"dingtalk_user_id"`
|
||||
}
|
||||
|
||||
func (UserDepartment) TableName() string { return "user_departments" }
|
||||
|
||||
// Visitor 访客信息
|
||||
type Visitor struct {
|
||||
BaseModel
|
||||
UserID *uint `gorm:"comment:关联用户ID(可为空)" json:"user_id"`
|
||||
Company string `gorm:"size:200;comment:访客公司名称" json:"company"`
|
||||
Name string `gorm:"size:50;not null;comment:访客姓名" json:"name"`
|
||||
Phone string `gorm:"size:20;not null;comment:访客电话" json:"phone"`
|
||||
Gender int `gorm:"comment:性别(0未知 1男 2女)" json:"gender"`
|
||||
IDType string `gorm:"size:20;comment:证件类型" json:"id_type"`
|
||||
IDNumber string `gorm:"size:50;comment:证件号码" json:"id_number"`
|
||||
FaceVerified bool `gorm:"default:false;comment:是否已完成人脸验证" json:"face_verified"`
|
||||
PhoneVerified bool `gorm:"default:false;comment:是否已完成手机验证" json:"phone_verified"`
|
||||
}
|
||||
|
||||
func (Visitor) TableName() string { return "visitors" }
|
||||
|
||||
// Appointment 预约
|
||||
type Appointment struct {
|
||||
BaseModel
|
||||
VisitUserID uint `gorm:"comment:被访用户ID" json:"visit_user_id"`
|
||||
CreatorUserID uint `gorm:"comment:创建人用户ID" json:"creator_user_id"`
|
||||
Creator *User `gorm:"foreignKey:CreatorUserID;comment:创建人信息" json:"creator,omitempty"`
|
||||
VisitType string `gorm:"size:50;comment:来访目的" json:"visit_type"`
|
||||
VisitStartTime time.Time `gorm:"comment:预计来访开始时间" json:"visit_start_time"`
|
||||
VisitEndTime time.Time `gorm:"comment:预计来访结束时间" json:"visit_end_time"`
|
||||
Status int `gorm:"default:0;comment:预约状态(0审核中 1通过 2拒绝 3取消)" json:"status"`
|
||||
Remark string `gorm:"type:text;comment:备注" json:"remark"`
|
||||
Comment string `gorm:"type:text;comment:审核意见" json:"comment"`
|
||||
VisitPurpose string `gorm:"type:text;comment:来访目的" json:"visit_purpose"`
|
||||
VisitorCompany string `gorm:"size:200;comment:来访单位名" json:"visitor_company"`
|
||||
QrCodeURL string `gorm:"size:500;comment:入场二维码URL" json:"qr_code_url"`
|
||||
VisitorInfo string `gorm:"type:jsonb;comment:所有访客信息(JSON数组)" json:"visitor_info"`
|
||||
CreatorInfo string `gorm:"type:jsonb;comment:创建人信息(JSON)" json:"creator_info"`
|
||||
EmployeeInfo string `gorm:"type:jsonb;comment:被访人信息(JSON)" json:"employee_info"`
|
||||
GoodsInfo string `gorm:"type:jsonb;comment:携带物品信息(JSON)" json:"goods_info"`
|
||||
AreasInfo string `gorm:"type:jsonb;comment:到访区域信息(JSON)" json:"areas_info"`
|
||||
VehicleInfo string `gorm:"type:jsonb;comment:车辆信息(JSON数组)" json:"vehicle_info"`
|
||||
}
|
||||
|
||||
func (Appointment) TableName() string { return "appointments" }
|
||||
|
||||
// InviteCode 邀请码
|
||||
type InviteCode struct {
|
||||
Code string `gorm:"primaryKey;size:100;comment:邀请码" json:"code"`
|
||||
VisitUserID uint `gorm:"index;comment:被访人用户ID" json:"visit_user_id"`
|
||||
VisitUser *User `gorm:"foreignKey:VisitUserID;comment:被访人信息" json:"visit_user,omitempty"`
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
ExpiresAt time.Time `gorm:"comment:过期时间" json:"expires_at"`
|
||||
}
|
||||
|
||||
func (InviteCode) TableName() string { return "invite_codes" }
|
||||
|
||||
// VisitRecord 访问记录
|
||||
type VisitRecord struct {
|
||||
BaseModel
|
||||
AppointmentID uint `gorm:"comment:关联的预约ID" json:"appointment_id"`
|
||||
Appointment *Appointment `gorm:"foreignKey:AppointmentID;comment:关联的预约信息" json:"appointment,omitempty"`
|
||||
GuardID uint `gorm:"comment:登记保安的用户ID" json:"guard_id"`
|
||||
Guard *User `gorm:"foreignKey:GuardID;comment:关联的保安用户信息" json:"guard,omitempty"`
|
||||
CheckType int `gorm:"comment:登记类型(1进场 2出场)" json:"check_type"`
|
||||
CheckTime time.Time `gorm:"comment:登记时间" json:"check_time"`
|
||||
IsValid bool `gorm:"default:true;comment:是否有效" json:"is_valid"`
|
||||
Remark string `gorm:"type:text;comment:备注" json:"remark"`
|
||||
}
|
||||
|
||||
func (VisitRecord) TableName() string { return "visit_records" }
|
||||
|
||||
// Invitation 邀请
|
||||
type Invitation struct {
|
||||
BaseModel
|
||||
EmployeeID uint `gorm:"comment:发起邀请的员工用户ID" json:"employee_id"`
|
||||
Employee *User `gorm:"foreignKey:EmployeeID;comment:关联的员工信息" json:"employee,omitempty"`
|
||||
InviteCode string `gorm:"size:100;uniqueIndex;not null;comment:邀请码" json:"invite_code"`
|
||||
VisitorName string `gorm:"size:50;comment:被邀请访客姓名" json:"visitor_name"`
|
||||
VisitorPhone string `gorm:"size:20;comment:被邀请访客电话" json:"visitor_phone"`
|
||||
VisitType string `gorm:"size:50;comment:来访目的" json:"visit_type"`
|
||||
VisitArea string `gorm:"size:200;comment:允许访问的区域" json:"visit_area"`
|
||||
ValidFrom time.Time `gorm:"comment:邀请生效时间" json:"valid_from"`
|
||||
ValidUntil time.Time `gorm:"comment:邀请失效时间" json:"valid_until"`
|
||||
MaxUseCount int `gorm:"default:1;comment:最大使用次数" json:"max_use_count"`
|
||||
UsedCount int `gorm:"default:0;comment:已使用次数" json:"used_count"`
|
||||
IsActive bool `gorm:"default:true;comment:是否有效" json:"is_active"`
|
||||
}
|
||||
|
||||
func (Invitation) TableName() string { return "invitations" }
|
||||
|
||||
// Banner 首页轮播图
|
||||
type Banner struct {
|
||||
BaseModel
|
||||
ImageURL string `gorm:"size:500;not null;comment:图片地址" json:"image_url"`
|
||||
JumpPath string `gorm:"size:500;comment:跳转链接" json:"jump_path"`
|
||||
IsJump bool `gorm:"default:false;comment:是否可跳转" json:"is_jump"`
|
||||
Sort int `gorm:"default:0;comment:排序号" json:"sort"`
|
||||
IsValid bool `gorm:"default:true;comment:是否有效" json:"is_valid"`
|
||||
}
|
||||
|
||||
func (Banner) TableName() string { return "banners" }
|
||||
|
||||
// Department 部门(树形结构)
|
||||
type Department struct {
|
||||
BaseModel
|
||||
Name string `gorm:"size:100;not null;comment:部门名称" json:"name"`
|
||||
ParentID *uint `gorm:"comment:上级部门ID" json:"parent_id"`
|
||||
Parent *Department `gorm:"foreignKey:ParentID;comment:上级部门" json:"parent,omitempty"`
|
||||
Children []Department `gorm:"foreignKey:ParentID;comment:子部门列表" json:"children,omitempty"`
|
||||
LeaderID *uint `gorm:"comment:部门负责人(部长)用户ID" json:"leader_id"`
|
||||
Leader *User `gorm:"foreignKey:LeaderID;comment:部门负责人信息" json:"leader,omitempty"`
|
||||
DeputyID *uint `gorm:"comment:部门副负责人(副部长)用户ID" json:"deputy_id"`
|
||||
Deputy *User `gorm:"foreignKey:DeputyID;comment:部门副负责人信息" json:"deputy,omitempty"`
|
||||
ApproverUserIDs string `gorm:"type:text;comment:部门指定审批人用户ID列表(JSON数组)" json:"approver_user_ids"`
|
||||
VpUserIDs string `gorm:"type:text;comment:分管领导用户ID列表(JSON数组)" json:"vp_user_ids"`
|
||||
Sort int `gorm:"default:0;comment:排序号" json:"sort"`
|
||||
Status int `gorm:"default:1;comment:状态(1启用 0禁用)" json:"status"`
|
||||
Description string `gorm:"size:200;comment:部门描述" json:"description"`
|
||||
}
|
||||
|
||||
func (Department) TableName() string { return "departments" }
|
||||
|
||||
// EmployeeCertification 角色认证申请
|
||||
type EmployeeCertification struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"not null;index;comment:申请人用户ID" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID;comment:关联的申请人信息" json:"user,omitempty"`
|
||||
RealName string `gorm:"size:50;not null;comment:真实姓名" json:"real_name"`
|
||||
Phone string `gorm:"size:20;not null;comment:手机号" json:"phone"`
|
||||
Role string `gorm:"size:20;default:员工;comment:角色类型(员工/保安)" json:"role"`
|
||||
Snapshot []string `gorm:"type:text;serializer:json;comment:材料截图URL列表" json:"snapshot"`
|
||||
Position string `gorm:"size:100;comment:职位" json:"position"`
|
||||
DepartmentID *uint `gorm:"comment:分配的部门ID(审核通过后设置)" json:"department_id"`
|
||||
Department *Department `gorm:"foreignKey:DepartmentID;comment:关联的部门信息" json:"department,omitempty"`
|
||||
Status int `gorm:"default:0;comment:审核状态(0待审核 1通过 2拒绝)" json:"status"`
|
||||
ApproverID *uint `gorm:"comment:审批人用户ID" json:"approver_id"`
|
||||
Approver *User `gorm:"foreignKey:ApproverID;comment:关联的审批人信息" json:"approver,omitempty"`
|
||||
Comment string `gorm:"type:text;comment:审核意见" json:"comment"`
|
||||
}
|
||||
|
||||
func (EmployeeCertification) TableName() string { return "employee_certifications" }
|
||||
|
||||
// Notice 系统通知
|
||||
type Notice struct {
|
||||
BaseModel
|
||||
Title string `gorm:"size:200;not null;comment:通知标题" json:"title"`
|
||||
Content string `gorm:"type:text;not null;comment:通知内容" json:"content"`
|
||||
IsValid bool `gorm:"default:true;comment:是否有效" json:"is_valid"`
|
||||
Sort int `gorm:"default:0;comment:排序号" json:"sort"`
|
||||
}
|
||||
|
||||
func (Notice) TableName() string { return "notices" }
|
||||
|
||||
// Notification 用户通知
|
||||
type Notification struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"index;not null;comment:接收用户ID" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID;comment:关联的用户信息" json:"user,omitempty"`
|
||||
Title string `gorm:"size:200;not null;comment:通知标题" json:"title"`
|
||||
Content string `gorm:"type:text;not null;comment:通知内容" json:"content"`
|
||||
Type string `gorm:"size:50;default:appointment;comment:通知类型(appointment/certification)" json:"type"`
|
||||
RefID *uint `gorm:"comment:关联业务ID" json:"ref_id"`
|
||||
IsRead bool `gorm:"default:false;comment:是否已读" json:"is_read"`
|
||||
}
|
||||
|
||||
func (Notification) TableName() string { return "notifications" }
|
||||
|
||||
// GoodsTemplate 物品模板
|
||||
type GoodsTemplate struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"index;not null;comment:用户ID" json:"user_id"`
|
||||
Name string `gorm:"size:200;not null;comment:物品名称" json:"name"`
|
||||
Quantity int `gorm:"default:1;comment:物品数量" json:"quantity"`
|
||||
Model string `gorm:"size:100;comment:物品型号" json:"model"`
|
||||
Remark string `gorm:"type:text;comment:物品备注" json:"remark"`
|
||||
}
|
||||
|
||||
func (GoodsTemplate) TableName() string { return "goods_templates" }
|
||||
|
||||
// VehicleInfo 用户车辆信息
|
||||
type VehicleInfo struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"index;not null;comment:用户ID" json:"user_id"`
|
||||
LicensePlate string `gorm:"size:20;comment:车牌号" json:"license_plate"`
|
||||
Brand string `gorm:"size:50;comment:品牌型号" json:"brand"`
|
||||
Color string `gorm:"size:20;comment:车辆颜色" json:"color"`
|
||||
}
|
||||
|
||||
func (VehicleInfo) TableName() string { return "vehicle_infos" }
|
||||
|
||||
// ==================== 工作流引擎模型 ====================
|
||||
|
||||
// ProcessDefinition 流程定义
|
||||
type ProcessDefinition struct {
|
||||
BaseModel
|
||||
ProcessName string `gorm:"size:100;not null;comment:流程名称" json:"process_name"`
|
||||
ProcessCode string `gorm:"uniqueIndex;size:50;not null;comment:流程编码" json:"process_code"`
|
||||
Source string `gorm:"size:100;comment:流程来源" json:"source"`
|
||||
Description string `gorm:"size:200;comment:流程描述" json:"description"`
|
||||
IsActive bool `gorm:"default:true;comment:是否启用" json:"is_active"`
|
||||
RevokeEvents string `gorm:"type:text;comment:撤销事件JSON" json:"revoke_events"`
|
||||
NodesJSON string `gorm:"type:text;comment:节点配置JSON" json:"nodes_json"`
|
||||
}
|
||||
|
||||
func (ProcessDefinition) TableName() string { return "process_definitions" }
|
||||
|
||||
// ProcessInstance 流程实例
|
||||
type ProcessInstance struct {
|
||||
BaseModel
|
||||
ProcessDefID uint `gorm:"index;not null;comment:流程定义ID" json:"process_def_id"`
|
||||
ProcessDef *ProcessDefinition `gorm:"foreignKey:ProcessDefID" json:"process_def,omitempty"`
|
||||
BusinessType string `gorm:"size:50;not null;comment:业务类型" json:"business_type"`
|
||||
BusinessID uint `gorm:"index;not null;comment:业务ID" json:"business_id"`
|
||||
Status int `gorm:"default:0;comment:实例状态(0运行中 1已完成 2已终止 3已撤销)" json:"status"`
|
||||
StarterID uint `gorm:"comment:发起人用户ID" json:"starter_id"`
|
||||
Starter *User `gorm:"foreignKey:StarterID" json:"starter,omitempty"`
|
||||
CurrentNodeID string `gorm:"size:50;comment:当前节点ID" json:"current_node_id"`
|
||||
Variables string `gorm:"type:text;comment:流程变量JSON" json:"variables"`
|
||||
FinishedAt *time.Time `gorm:"comment:完成时间" json:"finished_at"`
|
||||
}
|
||||
|
||||
func (ProcessInstance) TableName() string { return "process_instances" }
|
||||
|
||||
// Task 任务
|
||||
type Task struct {
|
||||
BaseModel
|
||||
InstanceID uint `gorm:"index;not null;comment:流程实例ID" json:"instance_id"`
|
||||
Instance *ProcessInstance `gorm:"foreignKey:InstanceID" json:"instance,omitempty"`
|
||||
NodeID string `gorm:"size:50;not null;comment:节点ID" json:"node_id"`
|
||||
NodeName string `gorm:"size:100;comment:节点名称" json:"node_name"`
|
||||
Status int `gorm:"default:0;comment:任务状态(0待处理 1已完成 2已拒绝 3已取消)" json:"status"`
|
||||
AssigneeID *uint `gorm:"comment:处理人用户ID" json:"assignee_id"`
|
||||
Assignee *User `gorm:"foreignKey:AssigneeID" json:"assignee,omitempty"`
|
||||
Comment string `gorm:"type:text;comment:处理意见" json:"comment"`
|
||||
HandledAt *time.Time `gorm:"comment:处理时间" json:"handled_at"`
|
||||
}
|
||||
|
||||
func (Task) TableName() string { return "tasks" }
|
||||
|
||||
// AdminUser 后台管理员账号
|
||||
type AdminUser struct {
|
||||
BaseModel
|
||||
Account string `gorm:"uniqueIndex;size:50;not null;comment:登录账号" json:"account"`
|
||||
Password string `gorm:"size:200;not null;comment:密码(SHA256)" json:"-"`
|
||||
Name string `gorm:"size:50;comment:管理员姓名" json:"name"`
|
||||
IsSuper bool `gorm:"default:false;comment:是否超级管理员" json:"is_super"`
|
||||
Status int `gorm:"default:1;comment:状态(1正常 0禁用)" json:"status"`
|
||||
}
|
||||
|
||||
func (AdminUser) TableName() string { return "admin_users" }
|
||||
|
||||
// VisitorArea 到访区域
|
||||
type VisitorArea struct {
|
||||
BaseModel
|
||||
Name string `gorm:"size:100;not null;comment:区域名称" json:"name"`
|
||||
Sort int `gorm:"default:0;comment:排序号" json:"sort"`
|
||||
Status int `gorm:"default:1;comment:状态(1启用 0禁用)" json:"status"`
|
||||
}
|
||||
|
||||
func (VisitorArea) TableName() string { return "visitor_areas" }
|
||||
|
||||
// VisitType 来访目的
|
||||
type VisitType struct {
|
||||
BaseModel
|
||||
Name string `gorm:"size:100;not null;comment:类型名称" json:"name"`
|
||||
Sort int `gorm:"default:0;comment:排序号" json:"sort"`
|
||||
Status int `gorm:"default:1;comment:状态(1启用 0禁用)" json:"status"`
|
||||
}
|
||||
|
||||
func (VisitType) TableName() string { return "visit_types" }
|
||||
|
||||
// PendingEmployee 待确认员工(通讯录导入)
|
||||
type PendingEmployee struct {
|
||||
BaseModel
|
||||
RealName string `gorm:"size:50;not null;index;comment:真实姓名" json:"real_name"`
|
||||
Phone string `gorm:"size:20;not null;index;comment:手机号" json:"phone"`
|
||||
DepartmentID uint `gorm:"not null;comment:部门ID" json:"department_id"`
|
||||
DepartmentName string `gorm:"size:100;comment:部门名称" json:"department_name"`
|
||||
Position string `gorm:"size:100;comment:职位" json:"position"`
|
||||
EmployeeType string `gorm:"size:30;default:employee;comment:员工类型" json:"employee_type"`
|
||||
DingTalkUserID string `gorm:"column:ding_talk_user_id;size:100;comment:钉钉用户unionId" json:"dingtalk_user_id"`
|
||||
IsMatched bool `gorm:"default:false;comment:是否已匹配用户" json:"is_matched"`
|
||||
MatchedUserID *uint `gorm:"comment:匹配到的用户ID" json:"matched_user_id"`
|
||||
MatchedAt *time.Time `gorm:"comment:匹配时间" json:"matched_at"`
|
||||
}
|
||||
|
||||
func (PendingEmployee) TableName() string { return "pending_employees" }
|
||||
|
||||
// GlobalApprover 全局兜底审批人
|
||||
type GlobalApprover struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"uniqueIndex;not null;comment:审批人用户ID" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID;comment:关联用户信息" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (GlobalApprover) TableName() string { return "global_approvers" }
|
||||
61
sc-lktx-backend/internal/common/response/response.go
Normal file
61
sc-lktx-backend/internal/common/response/response.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 统一 API 响应格式,所有接口使用此包返回数据
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response 统一响应结构:code=200 成功,其他为业务错误码
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// PageData 分页数据结构
|
||||
type PageData struct {
|
||||
List interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ===== 成功响应 =====
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{Code: 200, Message: "success", Data: data})
|
||||
}
|
||||
|
||||
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{Code: 200, Message: message, Data: data})
|
||||
}
|
||||
|
||||
func SuccessPage(c *gin.Context, list interface{}, total int64, page, pageSize int) {
|
||||
c.JSON(http.StatusOK, Response{Code: 200, Message: "success", Data: PageData{
|
||||
List: list, Total: total, Page: page, PageSize: pageSize,
|
||||
}})
|
||||
}
|
||||
|
||||
// ===== 错误响应 =====
|
||||
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
c.JSON(http.StatusOK, Response{Code: code, Message: message})
|
||||
}
|
||||
|
||||
func ErrorWithStatus(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.JSON(httpStatus, Response{Code: code, Message: message})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, message string) { Error(c, 400, message) }
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
ErrorWithStatus(c, http.StatusUnauthorized, 401, message)
|
||||
}
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
ErrorWithStatus(c, http.StatusForbidden, 403, message)
|
||||
}
|
||||
func NotFound(c *gin.Context, message string) { ErrorWithStatus(c, http.StatusNotFound, 404, message) }
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
ErrorWithStatus(c, http.StatusInternalServerError, 500, message)
|
||||
}
|
||||
61
sc-lktx-backend/internal/common/utils/context.go
Normal file
61
sc-lktx-backend/internal/common/utils/context.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 从 gin.Context 提取中间件注入的数据 + 分页工具
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetUserID 从 Context 获取用户 ID(支持 uint/float64/string 类型转换)
|
||||
func GetUserID(c *gin.Context) uint {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0
|
||||
}
|
||||
switch v := userID.(type) {
|
||||
case uint:
|
||||
return v
|
||||
case float64:
|
||||
return uint(v)
|
||||
case string:
|
||||
id, _ := strconv.ParseUint(v, 10, 64)
|
||||
return uint(id)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// GetRoleCode 获取主角色编码
|
||||
func GetRoleCode(c *gin.Context) string {
|
||||
roleCode, exists := c.Get("role_code")
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
return roleCode.(string)
|
||||
}
|
||||
|
||||
// HasRole 检查当前用户是否为指定角色
|
||||
func HasRole(c *gin.Context, roleCode string) bool {
|
||||
return GetRoleCode(c) == roleCode
|
||||
}
|
||||
|
||||
// ParsePagination 解析分页参数,默认 page=1, pageSize=10, 最大 1000
|
||||
func ParsePagination(c *gin.Context) (page, pageSize int) {
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "10")
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
pageSize, _ = strconv.Atoi(pageSizeStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 1000 {
|
||||
pageSize = 10
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOffset 计算 SQL offset
|
||||
func GetOffset(page, pageSize int) int {
|
||||
return (page - 1) * pageSize
|
||||
}
|
||||
101
sc-lktx-backend/internal/common/utils/jwt.go
Normal file
101
sc-lktx-backend/internal/common/utils/jwt.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// JWT Token 生成与解析 + 业务工具函数
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTClaims JWT 载荷,包含用户 ID 和角色列表
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
RoleCode string `json:"role_code"` // 角色(visitor/employee/guard)
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AdminJWTClaims 管理员 JWT 载荷
|
||||
type AdminJWTClaims struct {
|
||||
AdminID uint `json:"admin_id"`
|
||||
Account string `json:"account"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateAdminToken 生成管理员 JWT
|
||||
func GenerateAdminToken(adminID uint, account string, secret string, expireHours int) (string, error) {
|
||||
claims := AdminJWTClaims{
|
||||
AdminID: adminID,
|
||||
Account: account,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "lktx-admin",
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
// ParseAdminToken 解析管理员 JWT
|
||||
func ParseAdminToken(tokenString string, secret string) (*AdminJWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &AdminJWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims, ok := token.Claims.(*AdminJWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// GenerateToken 生成 JWT(单角色,兼容旧版)
|
||||
func GenerateToken(userID uint, roleCode string, secret string, expireHours int) (string, error) {
|
||||
claims := JWTClaims{
|
||||
UserID: userID,
|
||||
RoleCode: roleCode,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "lktx-mp",
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
// ParseToken 解析并验证 JWT
|
||||
func ParseToken(tokenString string, secret string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// GenerateInviteCode 生成 16 位随机邀请码
|
||||
func GenerateInviteCode() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// GenerateQRCodeContent 生成二维码内容
|
||||
func GenerateQRCodeContent(appointmentID uint) string {
|
||||
return fmt.Sprintf("LKTX:APPT:%d:%d", appointmentID, time.Now().Unix())
|
||||
}
|
||||
22
sc-lktx-backend/internal/common/utils/validator.go
Normal file
22
sc-lktx-backend/internal/common/utils/validator.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 数据校验工具
|
||||
package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
// ValidatePhone 验证中国大陆手机号(1 开头 + 3-9 + 9 位数字)
|
||||
func ValidatePhone(phone string) bool {
|
||||
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
|
||||
return matched
|
||||
}
|
||||
|
||||
// ValidateIDNumber 验证 18 位身份证号格式
|
||||
func ValidateIDNumber(idNumber string) bool {
|
||||
matched, _ := regexp.MatchString(`^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[\dXx]$`, idNumber)
|
||||
return matched
|
||||
}
|
||||
|
||||
// ValidateLicensePlate 验证中国大陆车牌号
|
||||
func ValidateLicensePlate(plate string) bool {
|
||||
matched, _ := regexp.MatchString(`^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$`, plate)
|
||||
return matched
|
||||
}
|
||||
Reference in New Issue
Block a user