'init'
This commit is contained in:
1697
sc-lktx-backend/internal/modules/appointment/handler.go
Normal file
1697
sc-lktx-backend/internal/modules/appointment/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
107
sc-lktx-backend/internal/modules/banner/handler.go
Normal file
107
sc-lktx-backend/internal/modules/banner/handler.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Banner 模块:首页轮播图接口
|
||||
package banner
|
||||
|
||||
import (
|
||||
"com.sclktx/m/v2/internal/common/model"
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BannerHandler Banner 处理器
|
||||
type BannerHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBannerHandler 创建 Banner 处理器
|
||||
func NewBannerHandler(db *gorm.DB) *BannerHandler {
|
||||
return &BannerHandler{db: db}
|
||||
}
|
||||
|
||||
// BannerVO Banner 视图对象(只返回前端需要的字段)
|
||||
type BannerVO struct {
|
||||
ImageURL string `json:"image_url"` // 图片地址
|
||||
JumpPath string `json:"jump_path"` // 跳转链接(空字符串表示不可跳转)
|
||||
IsJump bool `json:"is_jump"` // 是否可跳转
|
||||
}
|
||||
|
||||
// @Summary 获取首页轮播图列表
|
||||
// @Description 获取首页轮播图列表(公开接口)
|
||||
// @Tags 公共
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]banner.BannerVO} "success"
|
||||
// @Router /api/v1/banners [get]
|
||||
// GetBanners 获取首页轮播图列表(公开接口)
|
||||
func (h *BannerHandler) GetBanners(c *gin.Context) {
|
||||
var banners []model.Banner
|
||||
if err := h.db.Where("is_valid = ?", true).
|
||||
Order("sort ASC, id DESC").
|
||||
Find(&banners).Error; err != nil {
|
||||
response.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]BannerVO, 0, len(banners))
|
||||
for _, b := range banners {
|
||||
result = append(result, BannerVO{
|
||||
ImageURL: b.ImageURL,
|
||||
JumpPath: b.JumpPath,
|
||||
IsJump: b.IsJump,
|
||||
})
|
||||
}
|
||||
|
||||
// 如果没有数据,返回空数组而非 null
|
||||
if result == nil {
|
||||
result = []BannerVO{
|
||||
{
|
||||
ImageURL: "//mmbiz.qpic.cn/mmbiz_jpg/XekA4rfc3lcLOadSAicKvm3ZXFgn9TtZUOh5tSgaJeQrqs6JhWKfszMHBQhXaicPpgVM09U3dnHBfoEriaM9fKPXYCDvsB2fiaib4TAjXMGV2Ql0/0?from=appmsg",
|
||||
JumpPath: "",
|
||||
IsJump: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// NoticeVO 通知视图对象
|
||||
type NoticeVO struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// @Summary 获取有效通知列表
|
||||
// @Description 获取有效通知列表(公开接口)
|
||||
// @Tags 公共
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]banner.NoticeVO} "success"
|
||||
// @Router /api/v1/notices [get]
|
||||
// GetNotices 获取有效通知列表(公开接口)
|
||||
func (h *BannerHandler) GetNotices(c *gin.Context) {
|
||||
var notices []model.Notice
|
||||
h.db.Where("is_valid = ?", true).Order("sort ASC, id DESC").Find(¬ices)
|
||||
|
||||
result := make([]NoticeVO, 0, len(notices))
|
||||
for _, n := range notices {
|
||||
result = append(result, NoticeVO{
|
||||
ID: n.ID,
|
||||
Title: n.Title,
|
||||
Content: n.Content,
|
||||
})
|
||||
}
|
||||
if result == nil {
|
||||
result = []NoticeVO{}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册 Banner 路由(公开接口,无需认证)
|
||||
func (h *BannerHandler) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/banners", h.GetBanners)
|
||||
r.GET("/notices", h.GetNotices)
|
||||
}
|
||||
236
sc-lktx-backend/internal/modules/certification/handler.go
Normal file
236
sc-lktx-backend/internal/modules/certification/handler.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// 员工认证模块:提交认证申请、查询申请状态、撤回申请
|
||||
package certification
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/model"
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
"com.sclktx/m/v2/internal/common/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Handler 角色认证处理器
|
||||
type Handler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewHandler 创建角色认证处理器
|
||||
func NewHandler(db *gorm.DB) *Handler {
|
||||
return &Handler{db: db}
|
||||
}
|
||||
|
||||
// SubmitCertificationReq 提交认证申请请求
|
||||
type SubmitCertificationReq struct {
|
||||
RealName string `json:"real_name" binding:"required"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
Snapshot []string `json:"snapshot" binding:"required"`
|
||||
DepartmentID *uint `json:"department_id"`
|
||||
Position string `json:"position"`
|
||||
}
|
||||
|
||||
// SubmitCertification 提交角色认证申请
|
||||
// @Summary 提交角色认证申请
|
||||
// @Description 提交员工角色认证申请,需上传钉钉截图
|
||||
// @Tags 员工认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param body body SubmitCertificationReq true "认证申请请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/certification/submit [post]
|
||||
func (h *Handler) SubmitCertification(c *gin.Context) {
|
||||
var req SubmitCertificationReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
// 检查是否已有待审核或已通过的申请
|
||||
var existing model.EmployeeCertification
|
||||
if err := h.db.Where("user_id = ? AND status IN (0, 1)", userID).First(&existing).Error; err == nil {
|
||||
if existing.Status == 1 {
|
||||
response.BadRequest(c, "您已通过认证,无需重复申请")
|
||||
return
|
||||
}
|
||||
response.BadRequest(c, "您已提交申请,请等待审核")
|
||||
return
|
||||
}
|
||||
|
||||
cert := model.EmployeeCertification{
|
||||
UserID: userID,
|
||||
RealName: req.RealName,
|
||||
Phone: req.Phone,
|
||||
Role: req.Role,
|
||||
Snapshot: req.Snapshot,
|
||||
Position: req.Position,
|
||||
DepartmentID: req.DepartmentID,
|
||||
Status: 0,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&cert).Error; err != nil {
|
||||
response.InternalError(c, "提交失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "提交成功,请等待管理员审核", cert)
|
||||
}
|
||||
|
||||
// GetMyCertification 获取我的认证申请
|
||||
// @Summary 获取我的认证申请
|
||||
// @Description 获取当前用户的角色认证申请状态
|
||||
// @Tags 员工认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/certification/my [get]
|
||||
func (h *Handler) GetMyCertification(c *gin.Context) {
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
var cert model.EmployeeCertification
|
||||
if err := h.db.Preload("Department").Preload("Approver").Where("user_id = ?", userID).Order("created_at DESC").First(&cert).Error; err != nil {
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, cert)
|
||||
}
|
||||
|
||||
// WithdrawCertification 撤回认证申请
|
||||
// @Summary 撤回认证申请
|
||||
// @Description 撤回已提交但尚未处理的认证申请
|
||||
// @Tags 员工认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "认证申请ID"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/certification/{id}/withdraw [put]
|
||||
func (h *Handler) WithdrawCertification(c *gin.Context) {
|
||||
userID := utils.GetUserID(c)
|
||||
certID := c.Param("id")
|
||||
|
||||
var cert model.EmployeeCertification
|
||||
if err := h.db.First(&cert, certID).Error; err != nil {
|
||||
response.NotFound(c, "申请不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if cert.UserID != userID {
|
||||
response.Forbidden(c, "无权操作")
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Status != 0 {
|
||||
response.BadRequest(c, "该申请已处理,无法撤回")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&cert).Error; err != nil {
|
||||
response.InternalError(c, "撤回失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "已撤回", nil)
|
||||
}
|
||||
|
||||
// AutoVerifyReq 自动认证请求
|
||||
type AutoVerifyReq struct {
|
||||
RealName string `json:"real_name" binding:"required"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
}
|
||||
|
||||
// AutoVerify 自动认证:姓名+手机号 → 匹配 pending_employees → 直接认证为员工
|
||||
// @Summary 自动认证为员工
|
||||
// @Description 输入姓名和手机号,系统自动匹配通讯录导入数据,匹配成功直接认证为员工
|
||||
// @Tags 员工认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param body body AutoVerifyReq true "认证请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/certification/auto-verify [post]
|
||||
func (h *Handler) AutoVerify(c *gin.Context) {
|
||||
var req AutoVerifyReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请输入姓名和手机号")
|
||||
return
|
||||
}
|
||||
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
// 查当前用户角色
|
||||
var user model.User
|
||||
if err := h.db.First(&user, userID).Error; err != nil {
|
||||
response.InternalError(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
if user.Role == "employee" {
|
||||
response.BadRequest(c, "您已经是认证员工,无需重复申请")
|
||||
return
|
||||
}
|
||||
|
||||
// 查找 pending_employees
|
||||
var pending model.PendingEmployee
|
||||
if err := h.db.Where("phone = ?", req.Phone).First(&pending).Error; err != nil {
|
||||
response.BadRequest(c, "未找到您的认证信息,请联系管理员导入通讯录")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已被其他用户匹配
|
||||
if pending.IsMatched {
|
||||
if pending.MatchedUserID != nil && *pending.MatchedUserID == userID {
|
||||
// 已匹配当前用户但角色尚未升级 → 修复
|
||||
if user.Role != "employee" {
|
||||
goto doMatch
|
||||
}
|
||||
response.BadRequest(c, "您已认证成为员工,如有问题请联系管理员")
|
||||
} else {
|
||||
response.BadRequest(c, "该手机号已被其他用户认证,如有问题请联系管理员")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
doMatch:
|
||||
// 执行认证
|
||||
now := time.Now()
|
||||
ud := model.UserDepartment{
|
||||
UserID: userID,
|
||||
DepartmentID: pending.DepartmentID,
|
||||
DepartmentName: pending.DepartmentName,
|
||||
Position: pending.Position,
|
||||
EmployeeType: pending.EmployeeType,
|
||||
EmployeeStatus: 1,
|
||||
}
|
||||
h.db.Where("user_id = ?", userID).Assign(ud).FirstOrCreate(&model.UserDepartment{UserID: userID})
|
||||
h.db.Model(&user).Update("role", "employee")
|
||||
h.db.Model(&pending).Updates(map[string]interface{}{
|
||||
"is_matched": true,
|
||||
"matched_user_id": &userID,
|
||||
"matched_at": &now,
|
||||
})
|
||||
|
||||
// 如果用户还没有真实姓名,同步填入
|
||||
if user.RealName == "" {
|
||||
h.db.Model(&user).Update("real_name", req.RealName)
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "认证成功", gin.H{
|
||||
"department_name": pending.DepartmentName,
|
||||
"position": pending.Position,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册认证路由(需要 JWT 认证)
|
||||
func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/certification/submit", h.SubmitCertification)
|
||||
r.GET("/certification/my", h.GetMyCertification)
|
||||
r.PUT("/certification/:id/withdraw", h.WithdrawCertification)
|
||||
r.POST("/certification/auto-verify", h.AutoVerify)
|
||||
}
|
||||
266
sc-lktx-backend/internal/modules/upload/handler.go
Normal file
266
sc-lktx-backend/internal/modules/upload/handler.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// Upload 模块:图片上传(支持上传到微信服务器和 MinIO,公开接口,无需权限)
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
"com.sclktx/m/v2/internal/pkg/minio"
|
||||
"com.sclktx/m/v2/internal/pkg/wechat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UploadHandler 上传处理器
|
||||
type UploadHandler struct {
|
||||
wxClient *wechat.MiniProgramClient
|
||||
minioClient *minio.Client
|
||||
}
|
||||
|
||||
// NewUploadHandler 创建上传处理器
|
||||
func NewUploadHandler(wxClient *wechat.MiniProgramClient, minioClient *minio.Client) *UploadHandler {
|
||||
return &UploadHandler{
|
||||
wxClient: wxClient,
|
||||
minioClient: minioClient,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadImgResponse 上传图片返回
|
||||
type UploadImgResponse struct {
|
||||
URL string `json:"url"` // 微信返回的图片 URL
|
||||
}
|
||||
|
||||
// @Summary 上传图片到微信服务器
|
||||
// @Description 上传图片文件到微信服务器(公开接口,无需权限)
|
||||
// @Tags 文件上传
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "图片文件"
|
||||
// @Success 200 {object} response.Response{data=upload.UploadImgResponse} "success"
|
||||
// @Router /api/v1/upload/image [post]
|
||||
// UploadImage 上传图片文件到微信服务器(公开接口,无需权限)
|
||||
// POST /api/v1/upload/image
|
||||
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择要上传的图片文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 读取文件内容
|
||||
imageData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
response.InternalError(c, "读取文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件名,如果没有则生成一个
|
||||
filename := header.Filename
|
||||
if filename == "" {
|
||||
filename = "image_" + time.Now().Format("20060102150405") + ".jpg"
|
||||
}
|
||||
|
||||
// 确保文件名有扩展名
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if ext == "" {
|
||||
// 根据 Content-Type 猜测扩展名
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
filename += ".png"
|
||||
case "image/gif":
|
||||
filename += ".gif"
|
||||
case "image/webp":
|
||||
filename += ".webp"
|
||||
default:
|
||||
filename += ".jpg"
|
||||
}
|
||||
}
|
||||
|
||||
// 上传到微信服务器
|
||||
result, err := h.wxClient.UploadImg(imageData, filename)
|
||||
if err != nil {
|
||||
response.InternalError(c, "上传到微信失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, &UploadImgResponse{URL: result.URL})
|
||||
}
|
||||
|
||||
// UploadImageByURLRequest 通过链接上传请求
|
||||
type UploadImageByURLRequest struct {
|
||||
URL string `json:"url" binding:"required"` // 图片链接
|
||||
}
|
||||
|
||||
// @Summary 通过链接上传图片到微信服务器
|
||||
// @Description 通过图片链接下载后上传到微信服务器(公开接口,无需权限)
|
||||
// @Tags 文件上传
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body upload.UploadImageByURLRequest true "图片链接信息"
|
||||
// @Success 200 {object} response.Response{data=upload.UploadImgResponse} "success"
|
||||
// @Router /api/v1/upload/image-by-url [post]
|
||||
// UploadImageByURL 通过图片链接下载后上传到微信服务器(公开接口,无需权限)
|
||||
// POST /api/v1/upload/image-by-url
|
||||
func (h *UploadHandler) UploadImageByURL(c *gin.Context) {
|
||||
var req UploadImageByURLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请提供图片链接(url)")
|
||||
return
|
||||
}
|
||||
|
||||
// 下载图片(使用自定义请求,添加 User-Agent 避免被反爬拦截)
|
||||
httpReq, err := http.NewRequest("GET", req.URL, nil)
|
||||
if err != nil {
|
||||
response.InternalError(c, "创建请求失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
response.InternalError(c, "下载图片失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
response.InternalError(c, "下载图片失败,HTTP状态码: "+http.StatusText(resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
imageData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
response.InternalError(c, "读取图片数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 从 URL 中提取文件名
|
||||
filename := extractFilenameFromURL(req.URL)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
ext := getExtByContentType(contentType)
|
||||
if !strings.HasSuffix(strings.ToLower(filename), ext) && ext != "" {
|
||||
filename += ext
|
||||
}
|
||||
|
||||
// 上传到微信服务器
|
||||
result, err := h.wxClient.UploadImg(imageData, filename)
|
||||
if err != nil {
|
||||
response.InternalError(c, "上传到微信失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, &UploadImgResponse{URL: result.URL})
|
||||
}
|
||||
|
||||
// extractFilenameFromURL 从 URL 中提取文件名
|
||||
func extractFilenameFromURL(rawURL string) string {
|
||||
// 去掉查询参数
|
||||
if idx := strings.Index(rawURL, "?"); idx != -1 {
|
||||
rawURL = rawURL[:idx]
|
||||
}
|
||||
// 取最后一段作为文件名
|
||||
base := filepath.Base(rawURL)
|
||||
if base == "." || base == "/" || base == "" {
|
||||
return "image_" + time.Now().Format("20060102150405") + ".jpg"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// getExtByContentType 根据 Content-Type 获取文件扩展名
|
||||
func getExtByContentType(contentType string) string {
|
||||
switch {
|
||||
case strings.Contains(contentType, "image/png"):
|
||||
return ".png"
|
||||
case strings.Contains(contentType, "image/gif"):
|
||||
return ".gif"
|
||||
case strings.Contains(contentType, "image/webp"):
|
||||
return ".webp"
|
||||
case strings.Contains(contentType, "image/svg"):
|
||||
return ".svg"
|
||||
case strings.Contains(contentType, "image/jpeg"):
|
||||
return ".jpg"
|
||||
default:
|
||||
return ".jpg"
|
||||
}
|
||||
}
|
||||
|
||||
// UploadToMinioResponse 上传到 MinIO 返回
|
||||
type UploadToMinioResponse struct {
|
||||
URL string `json:"url"` // MinIO 中的图片 URL
|
||||
}
|
||||
|
||||
// @Summary 上传图片到MinIO对象存储
|
||||
// @Description 上传图片到 MinIO 对象存储(公开接口,无需权限)
|
||||
// @Tags 文件上传
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "图片文件"
|
||||
// @Success 200 {object} response.Response{data=upload.UploadToMinioResponse} "success"
|
||||
// @Router /api/v1/upload/minio [post]
|
||||
// UploadImageToMinio 上传图片到 MinIO 对象存储(公开接口,无需权限)
|
||||
// POST /api/v1/upload/minio
|
||||
func (h *UploadHandler) UploadImageToMinio(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择要上传的图片文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 读取文件内容
|
||||
imageData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
response.InternalError(c, "读取文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件名,如果没有则生成一个
|
||||
filename := header.Filename
|
||||
if filename == "" {
|
||||
filename = "image_" + time.Now().Format("20060102150405") + ".jpg"
|
||||
}
|
||||
|
||||
// 确保文件名有扩展名
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if ext == "" {
|
||||
// 根据 Content-Type 猜测扩展名
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
filename += ".png"
|
||||
case "image/gif":
|
||||
filename += ".gif"
|
||||
case "image/webp":
|
||||
filename += ".webp"
|
||||
default:
|
||||
filename += ".jpg"
|
||||
}
|
||||
}
|
||||
|
||||
// 上传到 MinIO
|
||||
ctx := context.Background()
|
||||
url, err := h.minioClient.UploadImage(ctx, imageData, filename)
|
||||
if err != nil {
|
||||
response.InternalError(c, "上传到 MinIO 失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, &UploadToMinioResponse{URL: url})
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册上传路由(公开接口,无需认证和权限)
|
||||
func (h *UploadHandler) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/upload/image", h.UploadImage)
|
||||
r.POST("/upload/image-by-url", h.UploadImageByURL)
|
||||
r.POST("/upload/minio", h.UploadImageToMinio)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ApproverResolver 审批人动态解析器
|
||||
type ApproverResolver struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewApproverResolver(db *gorm.DB) *ApproverResolver {
|
||||
return &ApproverResolver{db: db}
|
||||
}
|
||||
|
||||
// parseJSONIDs 解析 JSON 数组字符串为 uint 切片
|
||||
func parseJSONIDs(jsonStr string) []uint {
|
||||
if jsonStr == "" {
|
||||
return nil
|
||||
}
|
||||
var ids []uint
|
||||
if err := json.Unmarshal([]byte(jsonStr), &ids); err != nil {
|
||||
return nil
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// ResolveDeptApproverIDs 取部门指定审批人(approver_user_ids),空则返回空切片
|
||||
func (r *ApproverResolver) ResolveDeptApproverIDs(deptID uint) ([]uint, error) {
|
||||
var dept model.Department
|
||||
if err := r.db.First(&dept, deptID).Error; err != nil {
|
||||
return nil, fmt.Errorf("部门不存在: %w", err)
|
||||
}
|
||||
return parseJSONIDs(dept.ApproverUserIDs), nil
|
||||
}
|
||||
|
||||
// ResolveVPIDsByDepartment 取部门分管领导(vp_user_ids),空则返回空切片
|
||||
func (r *ApproverResolver) ResolveVPIDsByDepartment(deptID uint) ([]uint, error) {
|
||||
var dept model.Department
|
||||
if err := r.db.First(&dept, deptID).Error; err != nil {
|
||||
return nil, fmt.Errorf("部门不存在: %w", err)
|
||||
}
|
||||
return parseJSONIDs(dept.VpUserIDs), nil
|
||||
}
|
||||
|
||||
// GetGlobalApproverIDs 获取所有全局兜底审批人
|
||||
func (r *ApproverResolver) GetGlobalApproverIDs() ([]uint, error) {
|
||||
var approvers []model.GlobalApprover
|
||||
r.db.Find(&approvers)
|
||||
var ids []uint
|
||||
for _, a := range approvers {
|
||||
ids = append(ids, a.UserID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
556
sc-lktx-backend/internal/modules/workflow/engine.go
Normal file
556
sc-lktx-backend/internal/modules/workflow/engine.go
Normal file
@@ -0,0 +1,556 @@
|
||||
// 工作流引擎核心逻辑
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Engine 工作流引擎
|
||||
type Engine struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewEngine 创建工作流引擎
|
||||
func NewEngine(db *gorm.DB) *Engine {
|
||||
return &Engine{db: db}
|
||||
}
|
||||
|
||||
// Node 节点配置(从 nodes_json 解析)
|
||||
type Node struct {
|
||||
NodeID string `json:"node_id"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeType int `json:"node_type"` // 0-开始 1-审批 2-网关 3-结束
|
||||
PrevNodeIDs []string `json:"prev_node_ids"`
|
||||
UserIDs []string `json:"user_ids"`
|
||||
Roles []string `json:"roles"`
|
||||
GwConfig *GatewayConfig `json:"gw_config"`
|
||||
IsCosigned int `json:"is_cosigned"` // 0-否 1-会签
|
||||
NodeStartEvents []string `json:"node_start_events"`
|
||||
NodeEndEvents []string `json:"node_end_events"`
|
||||
TaskFinishEvents []string `json:"task_finish_events"`
|
||||
}
|
||||
|
||||
// GatewayConfig 网关配置
|
||||
type GatewayConfig struct {
|
||||
Conditions []Condition `json:"conditions"`
|
||||
InevitableNodes []string `json:"inevitable_nodes"`
|
||||
WaitForAllPrevNode int `json:"wait_for_all_prev_node"` // 0-否 1-是
|
||||
}
|
||||
|
||||
// Condition 条件分支
|
||||
type Condition struct {
|
||||
Expression string `json:"expression"`
|
||||
NodeID string `json:"node_id"`
|
||||
}
|
||||
|
||||
// parseNodes 解析流程定义的节点配置
|
||||
func (e *Engine) parseNodes(def *model.ProcessDefinition) ([]Node, error) {
|
||||
if def.NodesJSON == "" {
|
||||
return nil, errors.New("流程节点配置为空")
|
||||
}
|
||||
var nodes []Node
|
||||
if err := json.Unmarshal([]byte(def.NodesJSON), &nodes); err != nil {
|
||||
return nil, fmt.Errorf("解析节点配置失败: %w", err)
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// findStartNode 查找开始节点
|
||||
func findStartNode(nodes []Node) *Node {
|
||||
for i := range nodes {
|
||||
if nodes[i].NodeType == 0 {
|
||||
return &nodes[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findNextNodes 查找下一个节点
|
||||
func findNextNodes(nodes []Node, currentNodeID string) []Node {
|
||||
var next []Node
|
||||
for _, n := range nodes {
|
||||
for _, prevID := range n.PrevNodeIDs {
|
||||
if prevID == currentNodeID {
|
||||
next = append(next, n)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// findNodeByID 根据节点 ID 查找节点
|
||||
func findNodeByID(nodes []Node, nodeID string) *Node {
|
||||
for i := range nodes {
|
||||
if nodes[i].NodeID == nodeID {
|
||||
return &nodes[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseVariables 解析流程实例中的变量 JSON
|
||||
func (e *Engine) parseVariables(variablesJSON string) map[string]interface{} {
|
||||
var vars map[string]interface{}
|
||||
if variablesJSON != "" {
|
||||
json.Unmarshal([]byte(variablesJSON), &vars)
|
||||
}
|
||||
if vars == nil {
|
||||
vars = make(map[string]interface{})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
|
||||
// StartInstanceByCode 启动流程实例(按流程编码)
|
||||
func (e *Engine) StartInstanceByCode(processCode string, businessType string, businessID uint, starterID uint, variables map[string]interface{}) (*model.ProcessInstance, error) {
|
||||
var def model.ProcessDefinition
|
||||
if err := e.db.Where("process_code = ?", processCode).First(&def).Error; err != nil {
|
||||
return nil, fmt.Errorf("流程定义不存在: %s", processCode)
|
||||
}
|
||||
return e.StartInstance(def.ID, businessType, businessID, starterID, variables)
|
||||
}
|
||||
func (e *Engine) StartInstance(defID uint, businessType string, businessID uint, starterID uint, variables map[string]interface{}) (*model.ProcessInstance, error) {
|
||||
// 查询流程定义
|
||||
var def model.ProcessDefinition
|
||||
if err := e.db.First(&def, defID).Error; err != nil {
|
||||
return nil, errors.New("流程定义不存在")
|
||||
}
|
||||
if !def.IsActive {
|
||||
return nil, errors.New("流程定义已禁用")
|
||||
}
|
||||
|
||||
// 解析节点配置
|
||||
nodes, err := e.parseNodes(&def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查找开始节点
|
||||
startNode := findStartNode(nodes)
|
||||
if startNode == nil {
|
||||
return nil, errors.New("流程定义缺少开始节点")
|
||||
}
|
||||
|
||||
// 序列化变量
|
||||
variablesJSON := "{}"
|
||||
if variables != nil {
|
||||
b, _ := json.Marshal(variables)
|
||||
variablesJSON = string(b)
|
||||
}
|
||||
|
||||
// 创建流程实例
|
||||
instance := &model.ProcessInstance{
|
||||
ProcessDefID: def.ID,
|
||||
BusinessType: businessType,
|
||||
BusinessID: businessID,
|
||||
Status: 0, // 运行中
|
||||
StarterID: starterID,
|
||||
CurrentNodeID: startNode.NodeID,
|
||||
Variables: variablesJSON,
|
||||
}
|
||||
|
||||
if err := e.db.Create(instance).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建流程实例失败: %w", err)
|
||||
}
|
||||
|
||||
// 流转到下一个节点
|
||||
if err := e.moveToNextNodes(instance, nodes, startNode.NodeID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// moveToNextNodes 流转到下一个节点,创建任务(使用默认 DB)
|
||||
func (e *Engine) moveToNextNodes(instance *model.ProcessInstance, nodes []Node, currentNodeID string) error {
|
||||
return e.moveToNextNodesTx(e.db, instance, nodes, currentNodeID)
|
||||
}
|
||||
|
||||
// moveToNextNodesTx 流转到下一个节点(支持外部事务)
|
||||
func (e *Engine) moveToNextNodesTx(tx *gorm.DB, instance *model.ProcessInstance, nodes []Node, currentNodeID string) error {
|
||||
nextNodes := findNextNodes(nodes, currentNodeID)
|
||||
|
||||
// 如果没有下一个节点(到达结束节点),标记流程完成
|
||||
if len(nextNodes) == 0 {
|
||||
now := time.Now()
|
||||
if err := tx.Model(instance).Updates(map[string]interface{}{
|
||||
"status": 1, // 已完成
|
||||
"finished_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return e.updateAppointmentStatus(tx, instance, 1)
|
||||
}
|
||||
|
||||
// 为每个下一个节点创建任务
|
||||
for _, node := range nextNodes {
|
||||
switch node.NodeType {
|
||||
case 1: // 审批节点
|
||||
if err := e.createApprovalTasksTx(tx, instance, nodes, &node); err != nil {
|
||||
return err
|
||||
}
|
||||
case 3: // 结束节点
|
||||
now := time.Now()
|
||||
if err := tx.Model(instance).Updates(map[string]interface{}{
|
||||
"status": 1,
|
||||
"finished_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return e.updateAppointmentStatus(tx, instance, 1)
|
||||
case 2: // 网关节点 - 自动判断条件
|
||||
if err := e.handleGatewayNodeTx(tx, instance, nodes, &node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前节点
|
||||
if len(nextNodes) > 0 {
|
||||
tx.Model(instance).Update("current_node_id", nextNodes[0].NodeID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleGatewayNodeTx 处理网关节点(支持外部事务)
|
||||
func (e *Engine) handleGatewayNodeTx(tx *gorm.DB, instance *model.ProcessInstance, nodes []Node, gwNode *Node) error {
|
||||
if gwNode.GwConfig == nil {
|
||||
return e.moveToNextNodesTx(tx, instance, nodes, gwNode.NodeID)
|
||||
}
|
||||
|
||||
// 解析流程变量
|
||||
var variables map[string]interface{}
|
||||
if instance.Variables != "" {
|
||||
json.Unmarshal([]byte(instance.Variables), &variables)
|
||||
}
|
||||
if variables == nil {
|
||||
variables = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 评估条件
|
||||
for _, cond := range gwNode.GwConfig.Conditions {
|
||||
if evaluateCondition(cond.Expression, variables) {
|
||||
return e.moveToNextNodesTx(tx, instance, nodes, cond.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// 没有条件满足,走必经节点
|
||||
if len(gwNode.GwConfig.InevitableNodes) > 0 {
|
||||
return e.moveToNextNodesTx(tx, instance, nodes, gwNode.GwConfig.InevitableNodes[0])
|
||||
}
|
||||
|
||||
return e.moveToNextNodesTx(tx, instance, nodes, gwNode.NodeID)
|
||||
}
|
||||
|
||||
// createApprovalTasks 为审批节点创建任务(使用默认 DB)
|
||||
func (e *Engine) createApprovalTasks(instance *model.ProcessInstance, nodes []Node, node *Node) error {
|
||||
return e.createApprovalTasksTx(e.db, instance, nodes, node)
|
||||
}
|
||||
|
||||
// createApprovalTasksTx 为审批节点创建任务(支持外部事务)
|
||||
func (e *Engine) createApprovalTasksTx(tx *gorm.DB, instance *model.ProcessInstance, nodes []Node, node *Node) error {
|
||||
// 解析审批人:优先使用 user_ids,其次使用 roles
|
||||
var assigneeIDs []uint
|
||||
variables := e.parseVariables(instance.Variables)
|
||||
|
||||
if len(node.UserIDs) > 0 {
|
||||
for _, uidStr := range node.UserIDs {
|
||||
if strings.HasPrefix(uidStr, "$") {
|
||||
// $变量名 → 从流程变量中取值
|
||||
varName := uidStr[1:]
|
||||
if val, ok := variables[varName]; ok {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
assigneeIDs = append(assigneeIDs, uint(v))
|
||||
case uint:
|
||||
assigneeIDs = append(assigneeIDs, v)
|
||||
case int:
|
||||
assigneeIDs = append(assigneeIDs, uint(v))
|
||||
case string:
|
||||
// 先尝试解析为单个 ID
|
||||
if id, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||||
assigneeIDs = append(assigneeIDs, uint(id))
|
||||
} else {
|
||||
// 再尝试解析为 JSON 数组(如 "[1,2,3]")
|
||||
var ids []uint
|
||||
if err := json.Unmarshal([]byte(v), &ids); err == nil {
|
||||
assigneeIDs = append(assigneeIDs, ids...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 静态数字 ID
|
||||
var uid uint
|
||||
if _, err := fmt.Sscanf(uidStr, "%d", &uid); err == nil {
|
||||
assigneeIDs = append(assigneeIDs, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(assigneeIDs) == 0 && len(node.Roles) > 0 {
|
||||
// 根据角色编码查找用户
|
||||
var users []model.User
|
||||
tx.Where("role IN ?", node.Roles).Find(&users)
|
||||
for _, u := range users {
|
||||
assigneeIDs = append(assigneeIDs, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(assigneeIDs) == 0 {
|
||||
// 没有找到审批人 — 跳过此节点,自动流转到下一节点(适用于未配置分管领导等情况)
|
||||
return e.moveToNextNodesTx(tx, instance, nodes, node.NodeID)
|
||||
}
|
||||
|
||||
// 为每个审批人创建任务
|
||||
for _, assigneeID := range assigneeIDs {
|
||||
task := &model.Task{
|
||||
InstanceID: instance.ID,
|
||||
NodeID: node.NodeID,
|
||||
NodeName: node.NodeName,
|
||||
Status: 0, // 待处理
|
||||
AssigneeID: &assigneeID,
|
||||
}
|
||||
if err := tx.Create(task).Error; err != nil {
|
||||
return fmt.Errorf("创建任务失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleGatewayNode 处理网关节点
|
||||
func (e *Engine) handleGatewayNode(instance *model.ProcessInstance, nodes []Node, gwNode *Node) error {
|
||||
if gwNode.GwConfig == nil {
|
||||
return e.moveToNextNodes(instance, nodes, gwNode.NodeID)
|
||||
}
|
||||
|
||||
// 解析流程变量
|
||||
var variables map[string]interface{}
|
||||
if instance.Variables != "" {
|
||||
json.Unmarshal([]byte(instance.Variables), &variables)
|
||||
}
|
||||
if variables == nil {
|
||||
variables = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 评估条件
|
||||
for _, cond := range gwNode.GwConfig.Conditions {
|
||||
if evaluateCondition(cond.Expression, variables) {
|
||||
return e.moveToNextNodes(instance, nodes, cond.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// 没有条件满足,走必经节点
|
||||
if len(gwNode.GwConfig.InevitableNodes) > 0 {
|
||||
return e.moveToNextNodes(instance, nodes, gwNode.GwConfig.InevitableNodes[0])
|
||||
}
|
||||
|
||||
return e.moveToNextNodes(instance, nodes, gwNode.NodeID)
|
||||
}
|
||||
|
||||
// evaluateCondition 简单条件评估(支持 $days>=3 这样的表达式)
|
||||
func evaluateCondition(expression string, variables map[string]interface{}) bool {
|
||||
// 简单实现:解析 "$key op value" 格式
|
||||
var key string
|
||||
var op string
|
||||
var expected float64
|
||||
n, _ := fmt.Sscanf(expression, "$%s %s %f", &key, &op, &expected)
|
||||
if n != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
val, ok := variables[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var numVal float64
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
numVal = v
|
||||
case int:
|
||||
numVal = float64(v)
|
||||
case json.Number:
|
||||
numVal, _ = v.Float64()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
switch op {
|
||||
case ">=":
|
||||
return numVal >= expected
|
||||
case ">":
|
||||
return numVal > expected
|
||||
case "<=":
|
||||
return numVal <= expected
|
||||
case "<":
|
||||
return numVal < expected
|
||||
case "==":
|
||||
return numVal == expected
|
||||
case "!=":
|
||||
return numVal != expected
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ApproveTask 审批通过任务
|
||||
func (e *Engine) ApproveTask(taskID uint, userID uint, comment string) error {
|
||||
var task model.Task
|
||||
if err := e.db.First(&task, taskID).Error; err != nil {
|
||||
return errors.New("任务不存在")
|
||||
}
|
||||
if task.Status != 0 {
|
||||
return errors.New("任务已处理")
|
||||
}
|
||||
// 验证审批人身份
|
||||
if task.AssigneeID == nil || *task.AssigneeID != userID {
|
||||
return errors.New("您不是该任务的审批人")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 开启事务,保证一致性
|
||||
tx := e.db.Begin()
|
||||
|
||||
if err := tx.Model(&task).Updates(map[string]interface{}{
|
||||
"status": 1, // 已完成
|
||||
"comment": comment,
|
||||
"handled_at": now,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("更新任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取实例和流程定义
|
||||
var instance model.ProcessInstance
|
||||
if err := tx.First(&instance, task.InstanceID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
var def model.ProcessDefinition
|
||||
if err := tx.First(&def, instance.ProcessDefID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := e.parseNodes(&def)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// 查找当前节点的定义
|
||||
currentNode := findNodeByID(nodes, task.NodeID)
|
||||
needAllApprovers := currentNode != nil && currentNode.IsCosigned == 1
|
||||
|
||||
if needAllApprovers {
|
||||
// 会签模式(is_cosigned=1):需要所有任务都完成
|
||||
var pendingCount int64
|
||||
tx.Model(&model.Task{}).
|
||||
Where("instance_id = ? AND node_id = ? AND status = 0", task.InstanceID, task.NodeID).
|
||||
Count(&pendingCount)
|
||||
if pendingCount > 0 {
|
||||
tx.Commit()
|
||||
return nil // 等待其他审批人
|
||||
}
|
||||
}
|
||||
// 非会签模式(is_cosigned=0 或不设置):一人通过即流转下一节点
|
||||
// 取消本节点其他待处理任务,防止并发重复创建下一节点
|
||||
if !needAllApprovers {
|
||||
tx.Model(&model.Task{}).
|
||||
Where("instance_id = ? AND node_id = ? AND status = 0 AND id != ?", task.InstanceID, task.NodeID, task.ID).
|
||||
Update("status", 3) // 已取消
|
||||
}
|
||||
|
||||
// 流转到下一个节点(在同一事务中)
|
||||
if err := e.moveToNextNodesTx(tx, &instance, nodes, task.NodeID); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// RejectTask 审批拒绝任务
|
||||
func (e *Engine) RejectTask(taskID uint, userID uint, comment string) error {
|
||||
var task model.Task
|
||||
if err := e.db.First(&task, taskID).Error; err != nil {
|
||||
return errors.New("任务不存在")
|
||||
}
|
||||
if task.Status != 0 {
|
||||
return errors.New("任务已处理")
|
||||
}
|
||||
// 验证审批人身份
|
||||
if task.AssigneeID == nil || *task.AssigneeID != userID {
|
||||
return errors.New("您不是该任务的审批人")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
tx := e.db.Begin()
|
||||
|
||||
if err := tx.Model(&task).Updates(map[string]interface{}{
|
||||
"status": 2, // 已拒绝
|
||||
"comment": comment,
|
||||
"handled_at": now,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("更新任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 终止流程实例
|
||||
var instance model.ProcessInstance
|
||||
if err := tx.First(&instance, task.InstanceID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&instance).Updates(map[string]interface{}{
|
||||
"status": 2, // 已终止
|
||||
"finished_at": now,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新业务状态
|
||||
if err := e.updateAppointmentStatus(tx, &instance, 2); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// checkNodeCompletion 检查节点所有任务是否完成
|
||||
func (e *Engine) checkNodeCompletion(instanceID uint, nodeID string) (*Node, bool, error) {
|
||||
// 检查是否还有待处理的任务
|
||||
var pendingCount int64
|
||||
e.db.Model(&model.Task{}).
|
||||
Where("instance_id = ? AND node_id = ? AND status = 0", instanceID, nodeID).
|
||||
Count(&pendingCount)
|
||||
|
||||
return nil, pendingCount == 0, nil
|
||||
}
|
||||
|
||||
// updateAppointmentStatus 工作流完成后更新业务表状态
|
||||
func (e *Engine) updateAppointmentStatus(tx *gorm.DB, instance *model.ProcessInstance, apptStatus int) error {
|
||||
if instance.BusinessType != "appointment" {
|
||||
return nil // 只处理预约业务
|
||||
}
|
||||
return tx.Model(&model.Appointment{}).Where("id = ?", instance.BusinessID).Updates(map[string]interface{}{
|
||||
"status": apptStatus,
|
||||
}).Error
|
||||
}
|
||||
787
sc-lktx-backend/internal/modules/workflow/handler.go
Normal file
787
sc-lktx-backend/internal/modules/workflow/handler.go
Normal file
@@ -0,0 +1,787 @@
|
||||
// 工作流模块 HTTP Handler
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"com.sclktx/m/v2/internal/common/model"
|
||||
"com.sclktx/m/v2/internal/common/response"
|
||||
"com.sclktx/m/v2/internal/common/utils"
|
||||
"com.sclktx/m/v2/internal/pkg/dingtalk"
|
||||
"com.sclktx/m/v2/internal/pkg/wechat/templates"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Handler 工作流处理器
|
||||
type Handler struct {
|
||||
db *gorm.DB
|
||||
engine *Engine
|
||||
wxSend func(openid, templateID string, data map[string]string, page string) error
|
||||
dingTalk *dingtalk.Client
|
||||
}
|
||||
|
||||
// NewHandler 创建工作流处理器
|
||||
func NewHandler(db *gorm.DB, wxSend func(openid, templateID string, data map[string]string, page string) error, dingTalk *dingtalk.Client) *Handler {
|
||||
return &Handler{
|
||||
db: db,
|
||||
engine: NewEngine(db),
|
||||
wxSend: wxSend,
|
||||
dingTalk: dingTalk,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 请求/响应结构 ====================
|
||||
|
||||
// CreateDefinitionReq 创建流程定义请求
|
||||
type CreateDefinitionReq struct {
|
||||
ProcessName string `json:"process_name" binding:"required"`
|
||||
ProcessCode string `json:"process_code" binding:"required"`
|
||||
Source string `json:"source"`
|
||||
Description string `json:"description"`
|
||||
RevokeEvents string `json:"revoke_events"`
|
||||
NodesJSON string `json:"nodes_json"`
|
||||
}
|
||||
|
||||
// UpdateDefinitionReq 更新流程定义请求
|
||||
type UpdateDefinitionReq struct {
|
||||
ProcessName string `json:"process_name"`
|
||||
Source string `json:"source"`
|
||||
Description string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
RevokeEvents string `json:"revoke_events"`
|
||||
NodesJSON string `json:"nodes_json"`
|
||||
}
|
||||
|
||||
// StartInstanceReq 启动流程实例请求
|
||||
type StartInstanceReq struct {
|
||||
ProcessCode string `json:"process_code" binding:"required"`
|
||||
BusinessType string `json:"business_type" binding:"required"`
|
||||
BusinessID uint `json:"business_id" binding:"required"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
// TaskActionReq 任务操作请求
|
||||
type TaskActionReq struct {
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// ==================== 路由注册 ====================
|
||||
|
||||
// RegisterRoutes 注册工作流路由(需要 JWT 认证)
|
||||
func (h *Handler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
// 流程定义(管理员操作)
|
||||
router.GET("/workflow/definitions", h.ListDefinitions)
|
||||
router.GET("/workflow/definitions/:id", h.GetDefinition)
|
||||
router.POST("/workflow/definitions", h.CreateDefinition)
|
||||
router.PUT("/workflow/definitions/:id", h.UpdateDefinition)
|
||||
router.DELETE("/workflow/definitions/:id", h.DeleteDefinition)
|
||||
|
||||
// 流程实例
|
||||
router.POST("/workflow/instances", h.StartInstance)
|
||||
router.GET("/workflow/instances", h.ListInstances)
|
||||
router.GET("/workflow/instances/:id", h.GetInstanceDetail)
|
||||
|
||||
// 任务
|
||||
router.GET("/workflow/tasks/pending", h.GetPendingTasks)
|
||||
router.POST("/workflow/tasks/:id/approve", h.ApproveTask)
|
||||
router.POST("/workflow/tasks/:id/reject", h.RejectTask)
|
||||
}
|
||||
|
||||
// ==================== 流程定义管理 ====================
|
||||
|
||||
// ListDefinitions 获取流程定义列表
|
||||
// @Summary 获取流程定义列表
|
||||
// @Description 获取所有流程定义列表
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页条数" default(10)
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/definitions [get]
|
||||
func (h *Handler) ListDefinitions(c *gin.Context) {
|
||||
page, pageSize := utils.ParsePagination(c)
|
||||
|
||||
var defs []model.ProcessDefinition
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&model.ProcessDefinition{})
|
||||
query.Count(&total)
|
||||
query.Offset(utils.GetOffset(page, pageSize)).
|
||||
Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(&defs)
|
||||
|
||||
response.SuccessPage(c, defs, total, page, pageSize)
|
||||
}
|
||||
|
||||
// GetDefinition 获取流程定义详情
|
||||
// @Summary 获取流程定义详情
|
||||
// @Description 根据ID获取流程定义详情
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "流程定义ID"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/definitions/{id} [get]
|
||||
func (h *Handler) GetDefinition(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var def model.ProcessDefinition
|
||||
if err := h.db.First(&def, id).Error; err != nil {
|
||||
response.NotFound(c, "流程定义不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, def)
|
||||
}
|
||||
|
||||
// CreateDefinition 创建流程定义
|
||||
// @Summary 创建流程定义
|
||||
// @Description 创建一个新的流程定义(管理员操作)
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param body body CreateDefinitionReq true "创建流程定义请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/definitions [post]
|
||||
func (h *Handler) CreateDefinition(c *gin.Context) {
|
||||
var req CreateDefinitionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查编码唯一性
|
||||
var existCount int64
|
||||
h.db.Model(&model.ProcessDefinition{}).Where("process_code = ?", req.ProcessCode).Count(&existCount)
|
||||
if existCount > 0 {
|
||||
response.BadRequest(c, "流程编码已存在")
|
||||
return
|
||||
}
|
||||
|
||||
def := model.ProcessDefinition{
|
||||
ProcessName: req.ProcessName,
|
||||
ProcessCode: req.ProcessCode,
|
||||
Source: req.Source,
|
||||
Description: req.Description,
|
||||
IsActive: true,
|
||||
RevokeEvents: req.RevokeEvents,
|
||||
NodesJSON: req.NodesJSON,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&def).Error; err != nil {
|
||||
response.InternalError(c, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "创建成功", def)
|
||||
}
|
||||
|
||||
// UpdateDefinition 更新流程定义
|
||||
// @Summary 更新流程定义
|
||||
// @Description 更新指定流程定义的信息(管理员操作)
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "流程定义ID"
|
||||
// @Param body body UpdateDefinitionReq true "更新流程定义请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/definitions/{id} [put]
|
||||
func (h *Handler) UpdateDefinition(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var def model.ProcessDefinition
|
||||
if err := h.db.First(&def, id).Error; err != nil {
|
||||
response.NotFound(c, "流程定义不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateDefinitionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.ProcessName != "" {
|
||||
updates["process_name"] = req.ProcessName
|
||||
}
|
||||
if req.Source != "" {
|
||||
updates["source"] = req.Source
|
||||
}
|
||||
if req.Description != "" {
|
||||
updates["description"] = req.Description
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
if req.RevokeEvents != "" {
|
||||
updates["revoke_events"] = req.RevokeEvents
|
||||
}
|
||||
if req.NodesJSON != "" {
|
||||
updates["nodes_json"] = req.NodesJSON
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := h.db.Model(&def).Updates(updates).Error; err != nil {
|
||||
response.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "更新成功", nil)
|
||||
}
|
||||
|
||||
// DeleteDefinition 删除流程定义
|
||||
// @Summary 删除流程定义
|
||||
// @Description 删除指定的流程定义(管理员操作,存在运行中的实例时无法删除)
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "流程定义ID"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/definitions/{id} [delete]
|
||||
func (h *Handler) DeleteDefinition(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有运行中的实例
|
||||
var activeCount int64
|
||||
h.db.Model(&model.ProcessInstance{}).Where("process_def_id = ? AND status = 0", id).Count(&activeCount)
|
||||
if activeCount > 0 {
|
||||
response.BadRequest(c, "存在运行中的流程实例,无法删除")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&model.ProcessDefinition{}, id).Error; err != nil {
|
||||
response.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "删除成功", nil)
|
||||
}
|
||||
|
||||
// ==================== 流程实例管理 ====================
|
||||
|
||||
// StartInstance 启动流程实例
|
||||
// @Summary 启动流程实例
|
||||
// @Description 根据流程定义启动一个新的流程实例
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param body body StartInstanceReq true "启动流程实例请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/instances [post]
|
||||
func (h *Handler) StartInstance(c *gin.Context) {
|
||||
var req StartInstanceReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 查找流程定义
|
||||
var def model.ProcessDefinition
|
||||
if err := h.db.Where("process_code = ?", req.ProcessCode).First(&def).Error; err != nil {
|
||||
response.NotFound(c, "流程定义不存在")
|
||||
return
|
||||
}
|
||||
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
instance, err := h.engine.StartInstance(def.ID, req.BusinessType, req.BusinessID, userID, req.Variables)
|
||||
if err != nil {
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "启动成功", instance)
|
||||
}
|
||||
|
||||
// ListInstances 获取流程实例列表
|
||||
// @Summary 获取流程实例列表
|
||||
// @Description 获取所有流程实例列表,可按业务类型和状态筛选
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param business_type query string false "业务类型"
|
||||
// @Param status query int false "实例状态"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页条数" default(10)
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/instances [get]
|
||||
func (h *Handler) ListInstances(c *gin.Context) {
|
||||
page, pageSize := utils.ParsePagination(c)
|
||||
businessType := c.Query("business_type")
|
||||
businessIDStr := c.Query("business_id")
|
||||
statusStr := c.Query("status")
|
||||
|
||||
var instances []model.ProcessInstance
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&model.ProcessInstance{}).
|
||||
Preload("ProcessDef").
|
||||
Preload("Starter")
|
||||
if businessType != "" {
|
||||
query = query.Where("business_type = ?", businessType)
|
||||
}
|
||||
if businessIDStr != "" {
|
||||
businessID, _ := strconv.ParseUint(businessIDStr, 10, 64)
|
||||
if businessID > 0 {
|
||||
query = query.Where("business_id = ?", businessID)
|
||||
}
|
||||
}
|
||||
if statusStr != "" {
|
||||
status, _ := strconv.Atoi(statusStr)
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
query.Offset(utils.GetOffset(page, pageSize)).
|
||||
Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(&instances)
|
||||
|
||||
response.SuccessPage(c, instances, total, page, pageSize)
|
||||
}
|
||||
|
||||
// GetInstanceDetail 获取流程实例详情(含任务列表)
|
||||
// @Summary 获取流程实例详情
|
||||
// @Description 获取流程实例详情,包含关联的任务列表
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "流程实例ID"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/instances/{id} [get]
|
||||
func (h *Handler) GetInstanceDetail(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var instance model.ProcessInstance
|
||||
if err := h.db.Preload("ProcessDef").Preload("Starter").First(&instance, id).Error; err != nil {
|
||||
response.NotFound(c, "流程实例不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var tasks []model.Task
|
||||
h.db.Where("instance_id = ?", id).
|
||||
Preload("Assignee").
|
||||
Order("created_at ASC").
|
||||
Find(&tasks)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"instance": instance,
|
||||
"tasks": tasks,
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 任务管理 ====================
|
||||
|
||||
// GetPendingTasks 获取当前用户的待办任务
|
||||
// @Summary 获取待办任务
|
||||
// @Description 获取当前用户的待处理流程任务列表
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/tasks/pending [get]
|
||||
func (h *Handler) GetPendingTasks(c *gin.Context) {
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
var tasks []model.Task
|
||||
h.db.Where("assignee_id = ? AND status = 0", userID).
|
||||
Preload("Instance").
|
||||
Preload("Instance.ProcessDef").
|
||||
Preload("Assignee").
|
||||
Order("created_at DESC").
|
||||
Find(&tasks)
|
||||
|
||||
response.Success(c, tasks)
|
||||
}
|
||||
|
||||
// ApproveTask 审批通过任务
|
||||
// @Summary 审批通过任务
|
||||
// @Description 审批通过指定的流程任务
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "任务ID"
|
||||
// @Param body body TaskActionReq true "审批请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/tasks/{id}/approve [post]
|
||||
func (h *Handler) ApproveTask(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req TaskActionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := h.engine.ApproveTask(uint(id), userID, req.Comment); err != nil {
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 审批完成后发送通知
|
||||
h.sendAppointmentNotification(uint(id))
|
||||
h.sendDingTalkAfterApproval(uint(id), req.Comment)
|
||||
|
||||
response.SuccessWithMessage(c, "审批通过", nil)
|
||||
}
|
||||
|
||||
// RejectTask 审批拒绝任务
|
||||
// @Summary 审批拒绝任务
|
||||
// @Description 拒绝指定的流程任务
|
||||
// @Tags 工作流
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path uint true "任务ID"
|
||||
// @Param body body TaskActionReq true "拒绝请求"
|
||||
// @Success 200 {object} response.Response "success"
|
||||
// @Router /api/v1/workflow/tasks/{id}/reject [post]
|
||||
func (h *Handler) RejectTask(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req TaskActionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := h.engine.RejectTask(uint(id), userID, req.Comment); err != nil {
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 拒绝后发送通知
|
||||
h.sendAppointmentNotification(uint(id))
|
||||
h.sendDingTalkAfterRejection(uint(id), req.Comment)
|
||||
|
||||
response.SuccessWithMessage(c, "已拒绝", nil)
|
||||
}
|
||||
|
||||
// sendAppointmentNotification 审批结束后发送微信通知给申请人
|
||||
func (h *Handler) sendAppointmentNotification(taskID uint) {
|
||||
var task model.Task
|
||||
if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
if task.Instance == nil || task.Instance.BusinessType != "appointment" {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
var appointment model.Appointment
|
||||
if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
// 只发送已通过或已拒绝的通知
|
||||
if appointment.Status != 1 && appointment.Status != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
var creatorUser model.User
|
||||
if err := h.db.First(&creatorUser, appointment.CreatorUserID).Error; err != nil || creatorUser.Openid == "" {
|
||||
return
|
||||
}
|
||||
|
||||
resultText := "已通过"
|
||||
if appointment.Status == 2 {
|
||||
resultText = "已拒绝"
|
||||
}
|
||||
|
||||
// 解析 JSON 字段
|
||||
var employeeInfo map[string]interface{}
|
||||
json.Unmarshal([]byte(appointment.EmployeeInfo), &employeeInfo)
|
||||
var visitorInfo map[string]interface{}
|
||||
json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo)
|
||||
company, _ := visitorInfo["company"].(string)
|
||||
employeeName, _ := employeeInfo["name"].(string)
|
||||
|
||||
var areasInfo []string
|
||||
json.Unmarshal([]byte(appointment.AreasInfo), &areasInfo)
|
||||
areaText := strings.Join(areasInfo, "、")
|
||||
|
||||
// 格式化时间(微信 thing 类型字段限 20 字)
|
||||
timeStr := ""
|
||||
if !appointment.VisitStartTime.IsZero() {
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
timeStr = appointment.VisitStartTime.In(loc).Format("01-02 15:04") + "-" + appointment.VisitEndTime.In(loc).Format("15:04")
|
||||
}
|
||||
|
||||
// TMPL_RESULT: 访客申请结果通知(thing 字段限 20 字)
|
||||
trunc := func(s string) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) > 20 {
|
||||
return string(runes[:20])
|
||||
}
|
||||
return s
|
||||
}
|
||||
tmplID := templates.TMPL_RESULT
|
||||
if h.wxSend != nil {
|
||||
h.wxSend(creatorUser.Openid, tmplID, map[string]string{
|
||||
"thing1": trunc(resultText),
|
||||
"thing2": trunc(company),
|
||||
"thing3": trunc(timeStr),
|
||||
"thing8": trunc(employeeName),
|
||||
"thing9": trunc(areaText),
|
||||
}, fmt.Sprintf("/subpackages/appointment/appointment-detail?id=%d", appointment.ID))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// sendDingTalkAfterApproval 审批通过后发送钉钉消息
|
||||
func (h *Handler) sendDingTalkAfterApproval(taskID uint, comment string) {
|
||||
if h.dingTalk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var task model.Task
|
||||
if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
if task.Instance == nil || task.Instance.BusinessType != "appointment" || task.Instance.Status != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"task_id": taskID,
|
||||
"instance_id": task.InstanceID,
|
||||
}).Info("钉钉:审批通过,开始发送通知")
|
||||
|
||||
go func() {
|
||||
// 获取审批人信息
|
||||
var approverUser model.User
|
||||
if task.AssigneeID == nil {
|
||||
return
|
||||
}
|
||||
if err := h.db.First(&approverUser, *task.AssigneeID).Error; err != nil {
|
||||
logrus.WithError(err).WithField("task_id", taskID).Warn("钉钉:查询审批人信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取预约信息
|
||||
var appointment model.Appointment
|
||||
if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var visitorInfo struct {
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo)
|
||||
visitTime := ""
|
||||
if !appointment.VisitStartTime.IsZero() {
|
||||
visitTime = dingtalk.FormatCSTTime(appointment.VisitStartTime, appointment.VisitEndTime)
|
||||
}
|
||||
|
||||
// 1. 通知被取消的同节点审批人(同一节点非会签模式,其他人审批后当前节点其他人被取消)
|
||||
var cancelledTasks []model.Task
|
||||
if err := h.db.Where("instance_id = ? AND node_id = ? AND status = 3 AND id != ?",
|
||||
task.InstanceID, task.NodeID, task.ID).Find(&cancelledTasks).Error; err == nil && len(cancelledTasks) > 0 {
|
||||
var userIDs []string
|
||||
for _, ct := range cancelledTasks {
|
||||
if ct.AssigneeID != nil {
|
||||
var ud model.UserDepartment
|
||||
if err := h.db.Where("user_id = ?", *ct.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" {
|
||||
userIDs = append(userIDs, ud.DingTalkUserID)
|
||||
} else if err != nil {
|
||||
logrus.WithError(err).WithField("user_id", *ct.AssigneeID).Warn("钉钉:查询同节点取消人 UserDepartment 失败")
|
||||
} else {
|
||||
logrus.WithField("user_id", *ct.AssigneeID).Warn("钉钉:同节点取消人 DingTalkUserID 为空")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(userIDs) > 0 {
|
||||
title, markdown := dingtalk.BuildSameNodeCancelledMsg(approverUser.RealName, comment, visitTime)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"user_ids": userIDs,
|
||||
"title": title,
|
||||
}).Info("钉钉:发送同节点取消通知")
|
||||
if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil {
|
||||
logrus.WithError(err).Error("钉钉发送同节点取消通知失败")
|
||||
}
|
||||
} else {
|
||||
logrus.WithField("cancelled_count", len(cancelledTasks)).Info("钉钉:同节点已取消任务无有效钉钉ID")
|
||||
}
|
||||
} else {
|
||||
logrus.WithField("has_cancelled", len(cancelledTasks) > 0).Info("钉钉:无同节点取消任务")
|
||||
}
|
||||
|
||||
// 2. 通知下一个节点审批人
|
||||
var pendingTasks []model.Task
|
||||
if err := h.db.Where("instance_id = ? AND status = 0", task.InstanceID).Find(&pendingTasks).Error; err == nil && len(pendingTasks) > 0 {
|
||||
var userIDs []string
|
||||
for _, pt := range pendingTasks {
|
||||
if pt.AssigneeID != nil {
|
||||
var ud model.UserDepartment
|
||||
if err := h.db.Where("user_id = ?", *pt.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" {
|
||||
userIDs = append(userIDs, ud.DingTalkUserID)
|
||||
} else if err != nil {
|
||||
logrus.WithError(err).WithField("user_id", *pt.AssigneeID).Warn("钉钉:查询下一节点审批人 UserDepartment 失败")
|
||||
} else {
|
||||
logrus.WithField("user_id", *pt.AssigneeID).Warn("钉钉:下一节点审批人 DingTalkUserID 为空")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(userIDs) > 0 {
|
||||
msg := &dingtalk.AppointmentResultMessage{
|
||||
ApproverName: approverUser.RealName,
|
||||
Action: "通过",
|
||||
Comment: comment,
|
||||
AppointmentID: appointment.ID,
|
||||
VisitTime: visitTime,
|
||||
VisitorName: visitorInfo.Name,
|
||||
}
|
||||
title, markdown := dingtalk.BuildApproveNotificationMsg(msg)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"user_ids": userIDs,
|
||||
"title": title,
|
||||
}).Info("钉钉:发送下一节点审批人通知")
|
||||
if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil {
|
||||
logrus.WithError(err).Error("钉钉发送下一节点通知失败")
|
||||
}
|
||||
} else {
|
||||
logrus.WithField("pending_count", len(pendingTasks)).Info("钉钉:下一节点无有效钉钉ID的审批人")
|
||||
}
|
||||
} else {
|
||||
logrus.WithField("has_pending", len(pendingTasks) > 0).Info("钉钉:无下一节点待审批任务")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// sendDingTalkAfterRejection 审批拒绝后发送钉钉消息
|
||||
func (h *Handler) sendDingTalkAfterRejection(taskID uint, comment string) {
|
||||
if h.dingTalk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var task model.Task
|
||||
if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
if task.Instance == nil || task.Instance.BusinessType != "appointment" {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"task_id": taskID,
|
||||
"instance_id": task.InstanceID,
|
||||
}).Info("钉钉:审批拒绝,开始发送通知")
|
||||
|
||||
go func() {
|
||||
// 获取拒绝人信息
|
||||
var rejector model.User
|
||||
if task.AssigneeID == nil {
|
||||
return
|
||||
}
|
||||
if err := h.db.First(&rejector, *task.AssigneeID).Error; err != nil {
|
||||
logrus.WithError(err).WithField("task_id", taskID).Warn("钉钉:查询拒绝人信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取预约信息
|
||||
var appointment model.Appointment
|
||||
if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var visitorInfo struct {
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo)
|
||||
visitTime := ""
|
||||
if !appointment.VisitStartTime.IsZero() {
|
||||
visitTime = dingtalk.FormatCSTTime(appointment.VisitStartTime, appointment.VisitEndTime)
|
||||
}
|
||||
|
||||
// 查找本实例中其他待审批任务,通知其审批人流程已终止
|
||||
var otherPendingTasks []model.Task
|
||||
if err := h.db.Where("instance_id = ? AND status = 0 AND id != ?", task.InstanceID, task.ID).Find(&otherPendingTasks).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var userIDs []string
|
||||
for _, pt := range otherPendingTasks {
|
||||
if pt.AssigneeID != nil {
|
||||
var ud model.UserDepartment
|
||||
if err := h.db.Where("user_id = ?", *pt.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" {
|
||||
userIDs = append(userIDs, ud.DingTalkUserID)
|
||||
} else if err != nil {
|
||||
logrus.WithError(err).WithField("user_id", *pt.AssigneeID).Warn("钉钉:查询其他待审批人 UserDepartment 失败")
|
||||
} else {
|
||||
logrus.WithField("user_id", *pt.AssigneeID).Warn("钉钉:其他待审批人 DingTalkUserID 为空")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(userIDs) > 0 {
|
||||
msg := &dingtalk.AppointmentResultMessage{
|
||||
ApproverName: rejector.RealName,
|
||||
Action: "拒绝",
|
||||
Comment: comment,
|
||||
AppointmentID: appointment.ID,
|
||||
VisitTime: visitTime,
|
||||
VisitorName: visitorInfo.Name,
|
||||
}
|
||||
title, markdown := dingtalk.BuildRejectNotificationMsg(msg)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"user_ids": userIDs,
|
||||
"title": title,
|
||||
}).Info("钉钉:发送拒绝通知给其他待审批人")
|
||||
if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil {
|
||||
logrus.WithError(err).Error("钉钉发送拒绝通知失败")
|
||||
}
|
||||
} else {
|
||||
logrus.WithField("other_pending_count", len(otherPendingTasks)).Info("钉钉:拒绝后无其他待审批人需通知")
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user