e7c9b06056
学校请假表单由后台动态配置,可能包含本平台无法提供值的必填字段 (如"是否离校"单选、请假类型细分下拉)。原先这类情况会直接提交, 由学校端返回业务错误,用户看到的提示不明确。 新增 checkXsgzUnfilledRequired:提交前检查所有 required 字段是否已有值 (空串/null 均视为缺失),有缺失则不发请求,直接提示 「该校请假表单还有必填项无法自动填写:X、Y,请前往学校学工系统办理」, 并写入日志。请假与销假两条路径都已接入。 新增 5 个单测覆盖:有值不报、无值报字段名、空串算缺失、 无 fieldName 回退 fieldId、非必填不报。
563 lines
21 KiB
Go
563 lines
21 KiB
Go
package handler
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"log/slog"
|
||
"strings"
|
||
"time"
|
||
|
||
"cpdaily-new/internal/database"
|
||
"cpdaily-new/internal/model"
|
||
"cpdaily-new/internal/ratelimit"
|
||
cpd "cpdaily-new/internal/service/cpdaily"
|
||
"cpdaily-new/internal/utils"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ---------- 销假(请假销假)----------
|
||
|
||
// GetPendingLeaves 获取今日校园待销假列表(实时请求今日校园,附带规定销假位置)
|
||
func GetPendingLeaves(c *fiber.Ctx) error {
|
||
userID := c.Locals("userId").(uint)
|
||
|
||
var user model.SysUser
|
||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||
return utils.FailAuth(c, "用户不存在")
|
||
}
|
||
if user.Cookie == "" {
|
||
return utils.Fail(c, 400, "请先登录")
|
||
}
|
||
|
||
cookieMap := cpd.ParseCookieJSON(user.Cookie)
|
||
host := getSchoolHost(user.SchoolID)
|
||
|
||
// 三易拓新学工5(如山东农业大学):走学校学工系统的请假记录
|
||
if schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorXsgz {
|
||
sess, sErr := newXsgzSession(c.Context(), &user)
|
||
if sErr != nil {
|
||
slog.Error("建立学工系统会话失败", "userId", userID, "error", sErr)
|
||
return utils.Fail(c, 503, "学工系统登录失败: "+sErr.Error())
|
||
}
|
||
// 说明:学校的 /api/leave/record/batch-cancel-list 是**辅导员**接口
|
||
// (学生调用返回「当前用户没有辅导员销假权限」),因此学生端改用
|
||
// leave_page 拉全部记录,再由 ToLeaveItems 按状态标记 CanReport。
|
||
recs, _, pErr := sess.XsgzFetchLeavePage(c.Context(), 1, 50)
|
||
if pErr != nil {
|
||
slog.Error("获取学工系统请假记录失败", "userId", userID, "error", pErr)
|
||
return utils.Fail(c, 503, "获取请假记录失败: "+pErr.Error())
|
||
}
|
||
return utils.Success(c, sess.ToLeaveItems(recs))
|
||
}
|
||
|
||
// 其他独立学工系统:销假不经辅导猫,给出明确提示
|
||
if rejectExternalLeaveSystem(c, &user, cookieMap) {
|
||
return nil
|
||
}
|
||
|
||
// 应用套件分流:新套(leave-new)走独立链路
|
||
isNewFlavor := schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorNew
|
||
|
||
var items []cpd.LeaveItem
|
||
_, err := withCookieRefresh(&user, cookieMap, host, func(cm map[string]string) error {
|
||
if isNewFlavor {
|
||
// 套件判定用的是会话开始时解析的 cookie;续期后重新判定以保持准确
|
||
if schoolLeaveFlavor(&user, cm) == cpd.LeaveFlavorNew {
|
||
var e error
|
||
items, e = fetchLeaveNewPendingList(cm, host, &user)
|
||
return e
|
||
}
|
||
}
|
||
var e error
|
||
items, e = cpd.FetchLeaveList(cm, host)
|
||
return e
|
||
})
|
||
if err != nil {
|
||
slog.Error("获取待销假列表失败", "userId", userID, "error", err)
|
||
if isCpdailyAuthError(err) {
|
||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||
}
|
||
return utils.Fail(c, 503, "获取待销假列表失败: "+err.Error())
|
||
}
|
||
|
||
return utils.Success(c, items)
|
||
}
|
||
|
||
// ExecuteLeaveRequest 销假执行参数
|
||
type ExecuteLeaveRequest struct {
|
||
LeaveID string `json:"leaveId"` // 今日校园请假记录 ID
|
||
Longitude string `json:"longitude"` // 销假经度(必填)
|
||
Latitude string `json:"latitude"` // 销假纬度(必填)
|
||
Address string `json:"address"` // 销假地址(无规定位置时必填)
|
||
ImageURL string `json:"imageUrl"` // 销假图片 URL(可选)
|
||
ApproverWid string `json:"approverWid"` // 审批人 userWid(reportApplyFlow=1 时为必填)
|
||
// ProjectID 销假所属请假类型(三易拓新学工5 必传 UUID)
|
||
ProjectID string `json:"projectId"`
|
||
}
|
||
|
||
// ExecuteLeave 执行销假(SSE 同步执行:POST 请求 + 流式事件返回进度与结果)。
|
||
// 事件流:connecting → fetching_detail → submitting → success / task_error
|
||
func ExecuteLeave(c *fiber.Ctx) error {
|
||
userID := c.Locals("userId").(uint)
|
||
|
||
var req ExecuteLeaveRequest
|
||
if err := c.BodyParser(&req); err != nil {
|
||
return utils.FailParam(c, "请求参数错误")
|
||
}
|
||
req.LeaveID = strings.TrimSpace(req.LeaveID)
|
||
req.Longitude = strings.TrimSpace(req.Longitude)
|
||
req.Latitude = strings.TrimSpace(req.Latitude)
|
||
req.Address = strings.TrimSpace(req.Address)
|
||
req.ApproverWid = strings.TrimSpace(req.ApproverWid)
|
||
if req.LeaveID == "" || req.Longitude == "" || req.Latitude == "" {
|
||
return utils.FailParam(c, "销假经纬度不能为空")
|
||
}
|
||
|
||
var user model.SysUser
|
||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||
return utils.FailAuth(c, "用户不存在")
|
||
}
|
||
if user.Cookie == "" {
|
||
return utils.Fail(c, 400, "请先登录")
|
||
}
|
||
// 设备码后端硬校验:销假执行需携带设备码(对齐打卡侧拦截语义),
|
||
// 账密登录创建的账号可能未绑定设备码,空设备码会触发学校端风控异常
|
||
if strings.TrimSpace(user.MobileCode) == "" {
|
||
return utils.Fail(c, 400, "请先在个人中心绑定设备码后再执行销假")
|
||
}
|
||
// 三易拓新学工5(如山东农业大学):销假提交到学校学工系统
|
||
if schoolLeaveFlavor(&user, cpd.ParseCookieJSON(user.Cookie)) == cpd.LeaveFlavorXsgz {
|
||
return executeXsgzLeaveCancel(c, &user, &req)
|
||
}
|
||
// 其他独立学工系统:销假不经辅导猫
|
||
if rejectExternalLeaveSystem(c, &user, cpd.ParseCookieJSON(user.Cookie)) {
|
||
return nil
|
||
}
|
||
|
||
// SSE 头
|
||
c.Set("Content-Type", "text/event-stream")
|
||
c.Set("Cache-Control", "no-cache")
|
||
c.Set("Connection", "keep-alive")
|
||
c.Set("X-Accel-Buffering", "no")
|
||
|
||
// SSE 写入器:所有检查在流内进行,通过 SSE event 返回结果
|
||
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
|
||
sendEvent := func(event string, data map[string]interface{}) {
|
||
d, _ := json.Marshal(data)
|
||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, string(d))
|
||
w.Flush()
|
||
}
|
||
|
||
// 保存销假日志
|
||
saveLog := func(isSuccess bool, errMsg, title string) {
|
||
logEntry := model.CpLeaveLog{
|
||
UserID: userID,
|
||
LeaveID: req.LeaveID,
|
||
Title: title,
|
||
SchoolName: user.SchoolName,
|
||
Action: model.LeaveActionReport,
|
||
IsSuccess: isSuccess,
|
||
ErrorMessage: errMsg,
|
||
}
|
||
database.DB.Create(&logEntry)
|
||
}
|
||
|
||
// VIP 判定:VIP 且未过期则免扣次数直接执行
|
||
isVip := user.VipExpireAt != nil && user.VipExpireAt.After(time.Now())
|
||
|
||
// 本次销假扣减额度(后台可配置,允许小数与 0=不扣费)
|
||
deduct := getReportDeduct()
|
||
|
||
// 检查可用次数(VIP 免检查;0 额度时恒放行)
|
||
if !isVip && user.SignCount < deduct {
|
||
slog.Warn("销假次数不足,拒绝执行", "userId", userID, "signCount", user.SignCount, "deduct", deduct)
|
||
sendEvent("task_error", map[string]interface{}{"message": "可用次数不足,请购买CDK或通过邀请获得次数"})
|
||
return
|
||
}
|
||
|
||
// 限流:同一用户(销假为同步执行,无任务维度,按用户维度限流)
|
||
rateKey := fmt.Sprintf("leave:%d", userID)
|
||
rules := []ratelimit.Rule{
|
||
{Limit: 3, Window: 2 * time.Minute, Label: "2m"},
|
||
{Limit: 6, Window: 1 * time.Hour, Label: "1h"},
|
||
{Limit: 10, Window: 24 * time.Hour, Label: "1d"},
|
||
}
|
||
if ok, msg := ratelimit.AllowMulti(rateKey, rules); !ok {
|
||
slog.Warn("销假触发限流", "userId", userID, "msg", msg)
|
||
sendEvent("task_error", map[string]interface{}{"message": msg, "rateLimit": true})
|
||
return
|
||
}
|
||
|
||
// 1. 连接成功
|
||
sendEvent("connecting", map[string]interface{}{"status": "connecting", "message": "正在连接今日校园..."})
|
||
|
||
cookieMap := cpd.ParseCookieJSON(user.Cookie)
|
||
host := getSchoolHost(user.SchoolID)
|
||
|
||
// 新套(leave-new):独立销假链路(详情校验 → 销假提交)
|
||
if schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorNew {
|
||
sendEvent("fetching_detail", map[string]interface{}{"status": "fetching_detail", "message": "正在获取请假详情..."})
|
||
title := "销假"
|
||
if d, derr := cpd.FetchLeaveNewDetail(cookieMap, host, req.LeaveID); derr == nil {
|
||
title = leaveLogTitle(&cpd.LeaveDetail{LeaveType: d.TopicName}, req.LeaveID)
|
||
}
|
||
sendEvent("submitting", map[string]interface{}{"status": "submitting", "message": "正在提交销假..."})
|
||
ok, errMsg, needReLogin := leaveNewExecuteReport(&user, cookieMap, host,
|
||
req.LeaveID, req.Address, req.Longitude, req.Latitude, req.ImageURL, req.ApproverWid)
|
||
if !ok {
|
||
slog.Error("销假失败(新套)", "userId", userID, "leaveId", req.LeaveID, "error", errMsg)
|
||
saveLog(false, errMsg, title)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needReLogin})
|
||
return
|
||
}
|
||
saveLog(true, "", title)
|
||
if !isVip {
|
||
database.DB.Model(&model.SysUser{}).Where("id = ? AND sign_count >= ?", userID, deduct).
|
||
Update("sign_count", gorm.Expr("sign_count - ?", deduct))
|
||
msg := "销假成功!(已扣 " + fmtDeduct(deduct) + " 次)"
|
||
if deduct <= 0 {
|
||
msg = "销假成功!(免扣次数)"
|
||
}
|
||
sendEvent("success", map[string]interface{}{"status": "success", "message": msg})
|
||
} else {
|
||
sendEvent("success", map[string]interface{}{"status": "success", "message": "销假成功!(VIP 免扣次数)"})
|
||
}
|
||
slog.Info("销假成功(新套)", "userId", userID, "leaveId", req.LeaveID, "title", title, "isVip", isVip)
|
||
return
|
||
}
|
||
|
||
// 2. 获取请假详情
|
||
sendEvent("fetching_detail", map[string]interface{}{"status": "fetching_detail", "message": "正在获取请假详情..."})
|
||
detail, err := cpd.FetchLeaveDetail(cookieMap, host, req.LeaveID)
|
||
if err != nil && isCookieExpired(err) {
|
||
slog.Info("获取请假详情cookie过期,尝试自动刷新", "userId", userID, "leaveId", req.LeaveID)
|
||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||
detail, err = cpd.FetchLeaveDetail(cookieMap, host, req.LeaveID)
|
||
} else {
|
||
err = wrapNeedReLogin(refreshErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
errMsg := "获取请假详情失败: " + err.Error()
|
||
if needReLoginSignal(err) {
|
||
errMsg = "登录凭证已过期,请重新登录"
|
||
}
|
||
slog.Error("获取请假详情失败", "userId", userID, "leaveId", req.LeaveID, "error", err)
|
||
saveLog(false, errMsg, leaveLogTitle(detail, ""))
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needReLoginSignal(err)})
|
||
return
|
||
}
|
||
title := leaveLogTitle(detail, req.LeaveID)
|
||
|
||
// 销假地址:用户选择/填写的优先,否则兜底第一个规定销假位置地址;两者皆空时阻止提交
|
||
if req.Address == "" && len(detail.ReportPlaces) == 0 {
|
||
errMsg := "该请假未设置规定销假位置,请填写销假地址"
|
||
saveLog(false, errMsg, title)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg})
|
||
return
|
||
}
|
||
|
||
// 审批人:请假类型要求销假审批(reportApplyFlow=1)时必须选择审批人
|
||
if detail.ReportApplyFlow && req.ApproverWid == "" {
|
||
errMsg := "该请假类型销假需要选择审批人,请重新选择"
|
||
saveLog(false, errMsg, title)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needApprover": true})
|
||
return
|
||
}
|
||
|
||
// 3. 构造表单并提交销假
|
||
sendEvent("submitting", map[string]interface{}{"status": "submitting", "message": "正在提交销假..."})
|
||
form := cpd.BuildLeaveReportForm(detail, req.Address, req.ImageURL, req.ApproverWid)
|
||
err = cpd.SubmitLeaveReport(cookieMap, host, form, req.Longitude, req.Latitude, user.MobileCode, user.StudentID)
|
||
if err != nil && isCookieExpired(err) {
|
||
slog.Info("提交销假cookie过期,尝试自动刷新", "userId", userID, "leaveId", req.LeaveID)
|
||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||
err = cpd.SubmitLeaveReport(cookieMap, host, form, req.Longitude, req.Latitude, user.MobileCode, user.StudentID)
|
||
} else {
|
||
err = wrapNeedReLogin(refreshErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
errMsg := "销假失败: " + err.Error()
|
||
if needReLoginSignal(err) {
|
||
errMsg = "登录凭证已过期,请重新登录"
|
||
}
|
||
slog.Error("销假失败", "userId", userID, "leaveId", req.LeaveID, "error", err)
|
||
saveLog(false, errMsg, title)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needReLoginSignal(err)})
|
||
return
|
||
}
|
||
|
||
// 4. 成功:写日志 + 扣减可用次数(VIP 有效期内免扣)
|
||
saveLog(true, "", title)
|
||
if !isVip {
|
||
database.DB.Model(&model.SysUser{}).Where("id = ? AND sign_count >= ?", userID, deduct).
|
||
Update("sign_count", gorm.Expr("sign_count - ?", deduct))
|
||
msg := "销假成功!(已扣 " + fmtDeduct(deduct) + " 次)"
|
||
if deduct <= 0 {
|
||
msg = "销假成功!(免扣次数)"
|
||
}
|
||
sendEvent("success", map[string]interface{}{
|
||
"status": "success",
|
||
"message": msg,
|
||
})
|
||
} else {
|
||
sendEvent("success", map[string]interface{}{
|
||
"status": "success",
|
||
"message": "销假成功!(VIP 免扣次数)",
|
||
})
|
||
}
|
||
|
||
slog.Info("销假成功", "userId", userID, "leaveId", req.LeaveID, "title", title, "isVip", isVip)
|
||
})
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetLeaveApprovers 获取销假可选审批人(leaveType.reportApplyFlow=1 的请假类型)。
|
||
// 请求:{ leaveId };服务端自行取详情中的 leaveType/起止时间,前端无需重复传递。
|
||
func GetLeaveApprovers(c *fiber.Ctx) error {
|
||
userID := c.Locals("userId").(uint)
|
||
|
||
var req struct {
|
||
LeaveID string `json:"leaveId"`
|
||
}
|
||
if err := c.BodyParser(&req); err != nil {
|
||
return utils.FailParam(c, "请求参数错误")
|
||
}
|
||
req.LeaveID = strings.TrimSpace(req.LeaveID)
|
||
if req.LeaveID == "" {
|
||
return utils.FailParam(c, "leaveId 不能为空")
|
||
}
|
||
|
||
var user model.SysUser
|
||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||
return utils.FailAuth(c, "用户不存在")
|
||
}
|
||
if user.Cookie == "" {
|
||
return utils.Fail(c, 400, "请先登录")
|
||
}
|
||
|
||
cookieMap := cpd.ParseCookieJSON(user.Cookie)
|
||
host := getSchoolHost(user.SchoolID)
|
||
|
||
// 学校独立学工系统:销假审批人也不经辅导猫
|
||
if rejectExternalLeaveSystem(c, &user, cookieMap) {
|
||
return nil
|
||
}
|
||
|
||
// 新套:销假确认人(getReportApprovers by leaveId)
|
||
if schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorNew {
|
||
approvers, err := fetchLeaveNewReportApprovers(cookieMap, host, req.LeaveID, &user)
|
||
if err != nil && isCookieExpired(err) {
|
||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||
approvers, err = fetchLeaveNewReportApprovers(cookieMap, host, req.LeaveID, &user)
|
||
} else {
|
||
err = wrapNeedReLogin(refreshErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
slog.Error("获取销假审批人失败(新套)", "userId", userID, "leaveId", req.LeaveID, "error", err)
|
||
if needReLoginSignal(err) {
|
||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||
}
|
||
return utils.Fail(c, 503, "获取销假审批人失败: "+err.Error())
|
||
}
|
||
return utils.Success(c, approvers)
|
||
}
|
||
|
||
detail, err := cpd.FetchLeaveDetail(cookieMap, host, req.LeaveID)
|
||
if err != nil && isCookieExpired(err) {
|
||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||
detail, err = cpd.FetchLeaveDetail(cookieMap, host, req.LeaveID)
|
||
} else {
|
||
err = wrapNeedReLogin(refreshErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
slog.Error("获取请假详情失败(审批人)", "userId", userID, "leaveId", req.LeaveID, "error", err)
|
||
if needReLoginSignal(err) {
|
||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||
}
|
||
return utils.Fail(c, 503, "获取请假详情失败: "+err.Error())
|
||
}
|
||
if !detail.ReportApplyFlow {
|
||
return utils.Success(c, []cpd.LeaveApprover{})
|
||
}
|
||
|
||
approvers, err := cpd.FetchReportApprovers(cookieMap, host, detail.LeaveCodeNum, detail.StartTime, detail.EndTime)
|
||
if err != nil && isCookieExpired(err) {
|
||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||
approvers, err = cpd.FetchReportApprovers(cookieMap, host, detail.LeaveCodeNum, detail.StartTime, detail.EndTime)
|
||
} else {
|
||
err = wrapNeedReLogin(refreshErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
slog.Error("获取销假审批人失败", "userId", userID, "leaveId", req.LeaveID, "error", err)
|
||
if needReLoginSignal(err) {
|
||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||
}
|
||
return utils.Fail(c, 503, "获取销假审批人失败: "+err.Error())
|
||
}
|
||
|
||
return utils.Success(c, approvers)
|
||
}
|
||
|
||
// leaveLogTitle 销假日志标题:请假类型名 + "销假";类型名缺失时兜底用 leaveId 前缀避免标题为空
|
||
func leaveLogTitle(detail *cpd.LeaveDetail, leaveID string) string {
|
||
if detail != nil && detail.LeaveType != "" {
|
||
return detail.LeaveType + "销假"
|
||
}
|
||
if leaveID != "" {
|
||
return "销假(" + leaveID + ")"
|
||
}
|
||
return "销假"
|
||
}
|
||
|
||
// ListLeaveLogs 获取销假日志列表
|
||
func ListLeaveLogs(c *fiber.Ctx) error {
|
||
userID := c.Locals("userId").(uint)
|
||
|
||
var logs []model.CpLeaveLog
|
||
database.DB.Where("user_id = ?", userID).
|
||
Order("created_at DESC").
|
||
Limit(10).
|
||
Find(&logs)
|
||
|
||
return utils.Success(c, logs)
|
||
}
|
||
|
||
// DeleteLeaveLog 删除销假日志
|
||
func DeleteLeaveLog(c *fiber.Ctx) error {
|
||
userID := c.Locals("userId").(uint)
|
||
id := c.Params("id")
|
||
|
||
var log model.CpLeaveLog
|
||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&log).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return utils.Fail(c, 404, "日志不存在")
|
||
}
|
||
return utils.FailServer(c, "查询失败")
|
||
}
|
||
|
||
database.DB.Delete(&log)
|
||
return utils.Success(c, nil)
|
||
}
|
||
|
||
// executeXsgzLeaveCancel 三易拓新学工5(如山东农业大学)提交销假。
|
||
//
|
||
// 契约源自前端 ActionPopup.vue:
|
||
//
|
||
// POST /api/leave/student_action/application_submit (multipart)
|
||
// { originalId: <原假单ID>, projectId, requestType:"CANCELLATION",
|
||
// fields[], cancellationApplyInArea, inArea, address, longitude, latitude }
|
||
//
|
||
// 即销假与请假共用同一提交接口,靠 requestType 区分。
|
||
func executeXsgzLeaveCancel(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeaveRequest) error {
|
||
c.Set("Content-Type", "text/event-stream")
|
||
c.Set("Cache-Control", "no-cache")
|
||
c.Set("Connection", "keep-alive")
|
||
c.Set("X-Accel-Buffering", "no")
|
||
|
||
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
|
||
sendEvent := func(event string, data map[string]interface{}) {
|
||
d, _ := json.Marshal(data)
|
||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, string(d))
|
||
w.Flush()
|
||
}
|
||
saveLog := func(isSuccess bool, errMsg string) {
|
||
database.DB.Create(&model.CpLeaveLog{
|
||
UserID: user.ID,
|
||
LeaveID: req.LeaveID,
|
||
Title: "销假申请",
|
||
SchoolName: user.SchoolName,
|
||
Action: model.LeaveActionReport,
|
||
IsSuccess: isSuccess,
|
||
ErrorMessage: errMsg,
|
||
})
|
||
}
|
||
|
||
sendEvent("connecting", map[string]interface{}{"status": "connecting", "message": "正在连接学校学工系统..."})
|
||
|
||
sess, err := newXsgzSession(c.Context(), user)
|
||
if err != nil {
|
||
msg := "学工系统登录失败: " + err.Error()
|
||
slog.Error("xsgz 会话建立失败", "userId", user.ID, "error", err)
|
||
saveLog(false, msg)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": msg})
|
||
return
|
||
}
|
||
|
||
sendEvent("submitting", map[string]interface{}{"status": "submitting", "message": "正在提交销假申请..."})
|
||
|
||
// 与前端一致:先按 (projectId, requestType) 拉取该申请的表单字段定义,
|
||
// 作为 fields 提交(ActionPopup.vue: C.value = await form_fields_for_add({projectId, requestType}))
|
||
formFields, ffErr := sess.XsgzFetchFormFields(c.Context(), req.ProjectID, "CANCELLATION")
|
||
if ffErr != nil {
|
||
slog.Warn("xsgz 获取销假表单字段失败,按空字段提交", "userId", user.ID, "error", ffErr)
|
||
formFields = nil
|
||
}
|
||
// 销假表单通常含"销假说明/原因",用地址兜底填充可见字段
|
||
for _, f := range formFields {
|
||
label := strings.ToLower(mapStr(f, "fieldZh") + mapStr(f, "fieldName") + mapStr(f, "label"))
|
||
if (strings.Contains(label, "说明") || strings.Contains(label, "原因")) && req.Address != "" {
|
||
f["value"] = req.Address
|
||
}
|
||
}
|
||
// 提交前预检:必填项无法自动填写时明确指出,避免含糊失败
|
||
if missing := checkXsgzUnfilledRequired(formFields); len(missing) > 0 {
|
||
msg := "该校销假表单还有必填项无法自动填写:" + strings.Join(missing, "、") +
|
||
",请前往学校学工系统办理"
|
||
slog.Warn("xsgz 销假表单存在未填必填项", "userId", user.ID, "missing", missing)
|
||
saveLog(false, msg)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": msg})
|
||
return
|
||
}
|
||
|
||
body, err := sess.XsgzSubmitCancel(c.Context(), cpd.XsgzSubmitPayload{
|
||
ProjectID: req.ProjectID,
|
||
OriginalID: req.LeaveID,
|
||
Address: req.Address,
|
||
Longitude: req.Longitude,
|
||
Latitude: req.Latitude,
|
||
Fields: formFields,
|
||
})
|
||
if err != nil {
|
||
msg := "提交销假失败: " + err.Error()
|
||
slog.Error("xsgz 提交销假失败", "userId", user.ID, "error", err)
|
||
saveLog(false, msg)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": msg})
|
||
return
|
||
}
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
}
|
||
_ = json.Unmarshal(body, &result)
|
||
if result.Code != 0 {
|
||
msg := result.Message
|
||
if msg == "" {
|
||
msg = "提交销假失败"
|
||
}
|
||
saveLog(false, msg)
|
||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": msg})
|
||
return
|
||
}
|
||
|
||
saveLog(true, "")
|
||
sendEvent("success", map[string]interface{}{"status": "success", "message": "销假申请提交成功!"})
|
||
slog.Info("xsgz 销假申请成功", "userId", user.ID, "leaveId", req.LeaveID)
|
||
})
|
||
return nil
|
||
}
|