package appointment import ( "encoding/json" "fmt" "strconv" "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/modules/workflow" "com.sclktx/m/v2/internal/pkg/dingtalk" "com.sclktx/m/v2/internal/pkg/wechat" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "gorm.io/gorm" ) // AppointmentHandler 预约处理器 // 处理访客预约的创建、查询、审核、取消,以及员工搜索和邀请管理 type AppointmentHandler struct { db *gorm.DB wxClient *wechat.MiniProgramClient dingTalk *dingtalk.Client } func NewAppointmentHandler(db *gorm.DB, wxClient *wechat.MiniProgramClient, dingTalk *dingtalk.Client) *AppointmentHandler { return &AppointmentHandler{db: db, wxClient: wxClient, dingTalk: dingTalk} } // ============ 员工搜索 ============ // SearchEmployees 搜索公司接待人员 // @Summary 搜索公司接待人员 // @Description 根据关键字搜索员工信息 // @Tags 员工管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{keyword=string} true "搜索关键字" // @Success 200 {object} response.Response "员工列表" // @Router /appointment/employees/search [post] func (h *AppointmentHandler) SearchEmployees(c *gin.Context) { var req struct { Keyword string `json:"keyword"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } var employees []model.User query := h.db.Where("users.status = 1") if req.Keyword != "" { query = query.Joins("LEFT JOIN user_departments ON user_departments.user_id = users.id"). Where("users.real_name LIKE ? OR users.phone LIKE ? OR user_departments.department_name LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%") } if err := query.Select("users.*").Limit(20).Find(&employees).Error; err != nil { response.InternalError(c, "查询失败") return } // 批量查询部门信息 var userIDs []uint for _, e := range employees { userIDs = append(userIDs, e.ID) } deptMap := map[uint]model.UserDepartment{} if len(userIDs) > 0 { var depts []model.UserDepartment h.db.Where("user_id IN ?", userIDs).Find(&depts) for _, d := range depts { deptMap[d.UserID] = d } } // EmployeeVO 员工视图对象 type EmployeeVO struct { ID uint `json:"id"` // 员工ID(Employee表主键) UserID uint `json:"user_id"` // 关联的用户ID(User表主键) Name string `json:"name"` // 员工姓名 Department string `json:"department"` // 所属部门名称 DepartmentID *uint `json:"department_id"` // 所属部门ID Position string `json:"position"` // 职位标签 Phone string `json:"phone"` // 手机号 } var result []EmployeeVO for _, e := range employees { ud, hasDept := deptMap[e.ID] var deptName string var deptID *uint if hasDept { deptName = ud.DepartmentName deptID = &ud.DepartmentID } result = append(result, EmployeeVO{ ID: e.ID, UserID: e.ID, Name: e.RealName, Department: deptName, DepartmentID: deptID, Position: ud.Position, Phone: e.Phone, }) } response.Success(c, result) } // ============ 预约管理 ============ // CreateAppointmentRequest 创建预约请求参数 type CreateAppointmentRequest struct { EmployeeName string `json:"employee_name"` // 公司接待人员姓名 EmployeePhone string `json:"employee_phone"` // 公司接待人员手机号 VisitStartTime string `json:"visit_start_time" binding:"required"` // 预计来访开始时间(必填) VisitEndTime string `json:"visit_end_time" binding:"required"` // 预计来访结束时间(必填) VisitAreas []string `json:"visit_areas"` // 可访问区域列表 Goods []GoodsItem `json:"goods"` // 携带物品列表 Visitors []VisitorItem `json:"visitors"` // 所有外部来访人员(第一人为主要访客) Remark string `json:"remark"` // 备注 VisitPurpose string `json:"visit_purpose"` // 来访目的 HasVehicle bool `json:"has_vehicle"` // 是否有车辆 LicensePlates []string `json:"license_plates"` // 车牌号列表 } // GoodsItem 携带物品项 type GoodsItem struct { Name string `json:"name"` // 物品名称 Quantity int `json:"quantity"` // 物品数量 Model string `json:"model"` // 物品型号 Remark string `json:"remark"` // 物品备注 } // VisitorItem 同行访客 type VisitorItem struct { Name string `json:"name"` // 访客姓名 Phone string `json:"phone"` // 访客电话 Company string `json:"company"` // 公司名称 Gender int `json:"gender"` // 性别(0未知 1男 2女) IDType string `json:"id_type"` // 证件类型 IDNumber string `json:"id_number"` // 证件号码 } // CreateAppointment 创建预约 // @Summary 创建预约 // @Description 创建访客预约,包含访客信息、物品、同行人等 // @Tags 预约管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body CreateAppointmentRequest true "预约信息" // @Success 200 {object} response.Response{data=model.Appointment} "创建成功" // @Router /appointment/appointments [post] func (h *AppointmentHandler) CreateAppointment(c *gin.Context) { var req CreateAppointmentRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误: "+err.Error()) return } // 验证外部来访人员 if len(req.Visitors) == 0 { response.BadRequest(c, "请至少填写一位外部来访人员") return } if !utils.ValidatePhone(req.Visitors[0].Phone) { response.BadRequest(c, "手机号格式不正确") return } // 解析时间(优先带时区格式,兼容无时区) startTime, err := time.Parse(time.RFC3339, req.VisitStartTime) if err != nil { startTime, err = time.Parse("2006-01-02 15:04:05-07:00", req.VisitStartTime) if err != nil { startTime, err = time.Parse("2006-01-02 15:04:05", req.VisitStartTime) if err != nil { response.BadRequest(c, "开始时间格式错误") return } } } endTime, err := time.Parse(time.RFC3339, req.VisitEndTime) if err != nil { endTime, err = time.Parse("2006-01-02 15:04:05-07:00", req.VisitEndTime) if err != nil { endTime, err = time.Parse("2006-01-02 15:04:05", req.VisitEndTime) if err != nil { response.BadRequest(c, "结束时间格式错误") return } } } if endTime.Before(startTime) { response.BadRequest(c, "结束时间不能早于开始时间") return } // 获取当前创建者 creatorUserID := utils.GetUserID(c) var creatorUser model.User if err := h.db.First(&creatorUser, creatorUserID).Error; err != nil { response.BadRequest(c, "创建者不存在") return } // 创建或查找主访客(按手机号去重) mainVisitor := req.Visitors[0] var visitor model.Visitor result := h.db.Where("phone = ?", mainVisitor.Phone).First(&visitor) if result.Error != nil { visitor = model.Visitor{ UserID: &creatorUserID, Company: mainVisitor.Company, Name: mainVisitor.Name, Phone: mainVisitor.Phone, Gender: mainVisitor.Gender, PhoneVerified: false, } if err := h.db.Create(&visitor).Error; err != nil { response.InternalError(c, "创建访客信息失败") return } } else { updates := map[string]interface{}{ "name": mainVisitor.Name, "company": mainVisitor.Company, "gender": mainVisitor.Gender, } if req.HasVehicle && len(req.LicensePlates) > 0 { updates["license_plate"] = req.LicensePlates[0] } if visitor.UserID == nil { updates["user_id"] = &creatorUserID } h.db.Model(&visitor).Updates(updates) } // === 构建预约数据 === appointment := model.Appointment{ CreatorUserID: creatorUserID, VisitStartTime: startTime, VisitEndTime: endTime, Status: 0, // 审核中 Remark: req.Remark, VisitPurpose: req.VisitPurpose, QrCodeURL: "", // TODO: 生成二维码 } // 1. 访客信息 (visitor_info):所有访客 type visitorJSON struct { Name string `json:"name"` Phone string `json:"phone"` Company string `json:"company"` Gender int `json:"gender"` Visitors []VisitorItem `json:"visitors,omitempty"` } visitorInfo := visitorJSON{ Name: mainVisitor.Name, Phone: mainVisitor.Phone, Company: mainVisitor.Company, Gender: mainVisitor.Gender, Visitors: req.Visitors, } visitorInfoBytes, _ := json.Marshal(visitorInfo) appointment.VisitorInfo = string(visitorInfoBytes) // 2. 创建者信息 (creator_info) creatorInfo := map[string]interface{}{ "id": creatorUser.ID, "name": creatorUser.RealName, "phone": creatorUser.Phone, } if creatorUser.RealName == "" { creatorInfo["name"] = creatorUser.Nickname } creatorInfoBytes, _ := json.Marshal(creatorInfo) appointment.CreatorInfo = string(creatorInfoBytes) // 3. 被访人信息 (employee_info) employeeInfo := map[string]interface{}{ "name": req.EmployeeName, "phone": req.EmployeePhone, } employeeInfoBytes, _ := json.Marshal(employeeInfo) appointment.EmployeeInfo = string(employeeInfoBytes) // 4. 携带物品 (goods_info) if len(req.Goods) > 0 { goodsInfoBytes, _ := json.Marshal(req.Goods) appointment.GoodsInfo = string(goodsInfoBytes) } else { appointment.GoodsInfo = "[]" } // 5. 到访区域 (areas_info) if len(req.VisitAreas) > 0 { areasInfoBytes, _ := json.Marshal(req.VisitAreas) appointment.AreasInfo = string(areasInfoBytes) } else { appointment.AreasInfo = "[]" } // 6. 车辆信息 (vehicle_info) type vehicleItemJSON struct { Plate string `json:"plate"` } var vehicleList []vehicleItemJSON if req.HasVehicle && len(req.LicensePlates) > 0 { for _, plate := range req.LicensePlates { if plate != "" { vehicleList = append(vehicleList, vehicleItemJSON{Plate: plate}) } } } if vehicleList == nil { vehicleList = []vehicleItemJSON{} } vehicleInfoBytes, _ := json.Marshal(vehicleList) appointment.VehicleInfo = string(vehicleInfoBytes) // 开启事务 tx := h.db.Begin() // 创建预约 if err := tx.Create(&appointment).Error; err != nil { tx.Rollback() response.InternalError(c, "创建预约失败") return } // === 根据员工匹配情况选择审批流程 === wfEngine := workflow.NewEngine(h.db) resolver := workflow.NewApproverResolver(h.db) // 按手机号匹配员工 var matchedEmployee model.User var empUD model.UserDepartment var defaultApproverIDs []uint if err := h.db.Where("phone = ? AND role = ?", req.EmployeePhone, "employee").First(&matchedEmployee).Error; err == nil { // 匹配到员工 → 设置 VisitUserID tx.Model(&appointment).Update("visit_user_id", matchedEmployee.ID) // 查部门审批配置 if err := tx.Where("user_id = ?", matchedEmployee.ID).First(&empUD).Error; err == nil { deptApproverIDs, _ := resolver.ResolveDeptApproverIDs(empUD.DepartmentID) vpIDs, _ := resolver.ResolveVPIDsByDepartment(empUD.DepartmentID) if len(deptApproverIDs) > 0 || len(vpIDs) > 0 { // 有部门配置 → 走三级审批 deptApproverIDsJSON, _ := json.Marshal(deptApproverIDs) vpIDsJSON, _ := json.Marshal(vpIDs) _, err = wfEngine.StartInstanceByCode("visitor_approval", "appointment", appointment.ID, creatorUserID, map[string]interface{}{ "employee_id": matchedEmployee.ID, "dept_approver_ids": string(deptApproverIDsJSON), "vp_ids": string(vpIDsJSON), "department_id": empUD.DepartmentID, "department_name": empUD.DepartmentName, }) if err != nil { tx.Rollback() response.InternalError(c, "启动审批流程失败: "+err.Error()) return } goto committed } } // 匹配到员工但未配置部门审批人/VP → 走兜底 defaultApproverIDs, _ = resolver.GetGlobalApproverIDs() } else { // 未匹配到员工 → 走兜底 tx.Model(&appointment).Update("visit_user_id", 0) defaultApproverIDs, _ = resolver.GetGlobalApproverIDs() } // 启动兜底审批流程 if len(defaultApproverIDs) > 0 { defaultIDsJSON, _ := json.Marshal(defaultApproverIDs) _, err := wfEngine.StartInstanceByCode("visitor_approval_simple", "appointment", appointment.ID, creatorUserID, map[string]interface{}{ "default_approver_ids": string(defaultIDsJSON), }) if err != nil { tx.Rollback() response.InternalError(c, "启动审批流程失败: "+err.Error()) return } } // 兜底审批人也未配置 → 不启动审批,预约直接通过(或保持待审核) committed: tx.Commit() // 异步发送钉钉消息给被访员工(如果匹配到员工且有钉钉ID) if h.dingTalk != nil && matchedEmployee.ID > 0 { var empDept model.UserDepartment if err := h.db.Where("user_id = ?", matchedEmployee.ID).First(&empDept).Error; err != nil { logrus.WithFields(logrus.Fields{ "employee_id": matchedEmployee.ID, "employee_name": matchedEmployee.RealName, "appointment_id": appointment.ID, }).Warn("匹配到员工但未找到 UserDepartment 记录,跳过钉钉通知") } else if empDept.DingTalkUserID == "" { logrus.WithFields(logrus.Fields{ "employee_id": matchedEmployee.ID, "employee_name": matchedEmployee.RealName, "appointment_id": appointment.ID, }).Warn("员工 UserDepartment 记录中 DingTalkUserID 为空,跳过钉钉通知") } else { dingTalkUserID := empDept.DingTalkUserID logrus.WithFields(logrus.Fields{ "employee_id": matchedEmployee.ID, "employee_name": matchedEmployee.RealName, "dingtalk_user_id": dingTalkUserID, "appointment_id": appointment.ID, }).Info("准备发送钉钉通知给被访员工") go func() { // 从预约对象中解析访客信息 var vi struct { Name string `json:"name"` Phone string `json:"phone"` Company string `json:"company"` Gender int `json:"gender"` Visitors []struct { Name string `json:"name"` Phone string `json:"phone"` Company string `json:"company"` Gender int `json:"gender"` IDType string `json:"id_type"` IDNumber string `json:"id_number"` } `json:"visitors"` } json.Unmarshal([]byte(appointment.VisitorInfo), &vi) // 拼接所有访客表格行(访客1、访客2、访客3……) type visitorRow struct { Name string Company string Phone string IDCard string } var allVisitors []visitorRow allVisitors = append(allVisitors, visitorRow{ Name: vi.Name, Company: vi.Company, Phone: vi.Phone, }) for _, v := range vi.Visitors { if v.Name != "" && (v.Name != vi.Name || v.Phone != vi.Phone) { idCard := "" if v.IDType != "" && v.IDNumber != "" { idCard = v.IDType + " " + v.IDNumber } allVisitors = append(allVisitors, visitorRow{ Name: v.Name, Company: v.Company, Phone: v.Phone, IDCard: idCard, }) } } visitorList := "" for i, v := range allVisitors { idx := i + 1 line := fmt.Sprintf("**访客%d:** %s", idx, v.Name) if v.Phone != "" { line += "(" + v.Phone + ")" } if v.Company != "" { line += "
" + v.Company } if v.IDCard != "" { line += "
" + v.IDCard } visitorList += line + "
" } // 从预约对象中解析车辆信息 type vehicleItem struct { Plate string `json:"plate"` } var vehicles []vehicleItem json.Unmarshal([]byte(appointment.VehicleInfo), &vehicles) vehicleText := "" for _, v := range vehicles { if v.Plate != "" { vehicleText += "- " + v.Plate + "
" } } msg := &dingtalk.AppointmentMessage{ VisitorList: visitorList, EmployeeName: req.EmployeeName, VisitTime: dingtalk.FormatCSTTime(appointment.VisitStartTime, appointment.VisitEndTime), VisitPurpose: req.VisitPurpose, Remark: req.Remark, VehicleInfo: vehicleText, AppointmentID: appointment.ID, } title, markdown := dingtalk.BuildNewAppointmentMsg(msg) logrus.WithFields(logrus.Fields{ "dingtalk_user_id": dingTalkUserID, "title": title, "appointment_id": appointment.ID, }).Info("钉钉机器人群发消息开始") if err := h.dingTalk.BatchSendRobotMessage([]string{dingTalkUserID}, title, markdown); err != nil { logrus.WithError(err).WithFields(logrus.Fields{ "dingtalk_user_id": dingTalkUserID, "appointment_id": appointment.ID, }).Error("发送钉钉消息给被访员工失败") } else { logrus.WithFields(logrus.Fields{ "dingtalk_user_id": dingTalkUserID, "appointment_id": appointment.ID, }).Info("发送钉钉消息给被访员工成功") } }() } } response.SuccessWithMessage(c, "预约创建成功", appointment) } // GetMyAppointments 我的预约列表(包括我创建的 + 别人预约我的) // @Summary 我的预约列表 // @Description 获取当前用户的预约列表,包括我创建的和别人预约我的 // @Tags 预约管理 // @Accept json // @Produce json // @Security BearerAuth // @Param status query integer false "预约状态筛选" // @Success 200 {object} response.Response{data=[]model.Appointment} "预约列表" // @Router /appointment/appointments/my [get] func (h *AppointmentHandler) GetMyAppointments(c *gin.Context) { userID := utils.GetUserID(c) page, pageSize := utils.ParsePagination(c) statusStr := c.DefaultQuery("status", "") typeFilter := c.DefaultQuery("type", "") // created / visited / approvable / mine var appointments []model.Appointment var total int64 query := h.db.Model(&model.Appointment{}) if typeFilter == "created" { query = query.Where("creator_user_id = ?", userID) } else if typeFilter == "visited" { query = query.Where("visit_user_id = ?", userID) } else if typeFilter == "approvable" { // 查询当前用户在工作流中有待审批任务的预约 subQuery := h.db.Table("process_instances"). Select("process_instances.business_id"). Joins("JOIN tasks ON tasks.instance_id = process_instances.id"). Where("process_instances.business_type = ? AND tasks.assignee_id = ? AND tasks.status = ?", "appointment", userID, 0) query = query.Where("id IN (?)", subQuery) } else if typeFilter == "mine" { // 预约我的:我是被访人 OR 我在工作流中有审批任务 subQuery := h.db.Table("process_instances"). Select("process_instances.business_id"). Joins("JOIN tasks ON tasks.instance_id = process_instances.id"). Where("process_instances.business_type = ? AND tasks.assignee_id = ?", "appointment", userID) query = query.Where("visit_user_id = ? OR id IN (?)", userID, subQuery) } else { query = query.Where("creator_user_id = ? OR visit_user_id = ?", userID, userID) } 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(&appointments) response.SuccessPage(c, appointments, total, page, pageSize) } // GetAllAppointments 全部预约列表(员工专属) // @Summary 全部预约列表 // @Description 获取所有预约申请,按申请时间降序排列(仅员工可用) // @Tags 预约管理 // @Accept json // @Produce json // @Security BearerAuth // @Param page query integer false "页码" // @Param page_size query integer false "每页数量" // @Param status query integer false "预约状态筛选" // @Success 200 {object} response.Response{data=[]model.Appointment} "预约列表" // @Router /appointment/appointments/all [get] func (h *AppointmentHandler) GetAllAppointments(c *gin.Context) { page, pageSize := utils.ParsePagination(c) statusStr := c.DefaultQuery("status", "") var appointments []model.Appointment var total int64 query := h.db.Model(&model.Appointment{}) 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(&appointments) response.SuccessPage(c, appointments, total, page, pageSize) } // GetAppointmentDetail 预约详情 // @Summary 获取预约详情 // @Description 根据ID获取预约详细信息 // @Tags 预约管理 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "预约ID" // @Success 200 {object} response.Response{data=model.Appointment} "预约详情" // @Router /appointment/appointments/{id} [get] func (h *AppointmentHandler) GetAppointmentDetail(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var appointment model.Appointment if err := h.db.First(&appointment, id).Error; err != nil { response.NotFound(c, "预约不存在") return } // 解析 JSON 字符串字段 var visitors []map[string]interface{} if err := json.Unmarshal([]byte(appointment.VisitorInfo), &visitors); err != nil { // 兼容旧格式:包含顶层 name/phone + 嵌套 visitors 数组 var old struct { Name string `json:"name"` Phone string `json:"phone"` Company string `json:"company"` Gender int `json:"gender"` Visitors []map[string]interface{} `json:"visitors"` } if err2 := json.Unmarshal([]byte(appointment.VisitorInfo), &old); err2 == nil && len(old.Visitors) > 0 { visitors = old.Visitors } else { visitors = []map[string]interface{}{} } } var creatorInfo map[string]interface{} json.Unmarshal([]byte(appointment.CreatorInfo), &creatorInfo) if creatorInfo == nil { creatorInfo = map[string]interface{}{} } var employeeInfo map[string]interface{} json.Unmarshal([]byte(appointment.EmployeeInfo), &employeeInfo) if employeeInfo == nil { employeeInfo = map[string]interface{}{} } var goods []map[string]interface{} json.Unmarshal([]byte(appointment.GoodsInfo), &goods) if goods == nil { goods = []map[string]interface{}{} } var areas []string json.Unmarshal([]byte(appointment.AreasInfo), &areas) if areas == nil { areas = []string{} } var vehicles []map[string]interface{} if err := json.Unmarshal([]byte(appointment.VehicleInfo), &vehicles); err != nil { // 兼容旧格式:{"license_plates": [...]} var old map[string]interface{} if err2 := json.Unmarshal([]byte(appointment.VehicleInfo), &old); err2 == nil { if plates, ok := old["license_plates"]; ok { if pArr, ok := plates.([]interface{}); ok { for _, p := range pArr { vehicles = append(vehicles, map[string]interface{}{ "plate": p, }) } } } } } if vehicles == nil { vehicles = []map[string]interface{}{} } // 保安角色查看时对手机号脱敏 if utils.GetRoleCode(c) == "guard" { maskPhone := func(phone string) string { if len(phone) == 11 { return phone[:3] + "****" + phone[7:] } if len(phone) > 3 { return phone[:3] + "****" + phone[len(phone)-4:] } return phone } for i, v := range visitors { if phone, ok := v["phone"].(string); ok { visitors[i]["phone"] = maskPhone(phone) } } if phone, ok := creatorInfo["phone"].(string); ok { creatorInfo["phone"] = maskPhone(phone) } if phone, ok := employeeInfo["phone"].(string); ok { employeeInfo["phone"] = maskPhone(phone) } } resp := map[string]interface{}{ "id": appointment.ID, "created_at": appointment.CreatedAt, "updated_at": appointment.UpdatedAt, "visit_user_id": appointment.VisitUserID, "creator_user_id": appointment.CreatorUserID, "visit_type": appointment.VisitType, "visit_start_time": appointment.VisitStartTime, "visit_end_time": appointment.VisitEndTime, "status": appointment.Status, "remark": appointment.Remark, "comment": appointment.Comment, "visit_purpose": appointment.VisitPurpose, "visitor_company": appointment.VisitorCompany, "qr_code_url": appointment.QrCodeURL, "visitors": visitors, "creator_info": creatorInfo, "employee_info": employeeInfo, "goods": goods, "areas": areas, "vehicles": vehicles, } response.Success(c, resp) } // GetApprovalSteps 获取预约的审批步骤列表 // ============ 邀请管理 ============ // CreateInvitation 生成邀请链接 // @Summary 生成邀请链接 // @Description 员工生成访客邀请链接 // @Tags 邀请管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{visitor_name=string,visitor_phone=string,visit_type=string,visit_area=string,valid_days=int,max_use_count=int} true "邀请信息" // @Success 200 {object} response.Response{data=model.Invitation} "创建成功" // @Router /appointment/invitations [post] func (h *AppointmentHandler) CreateInvitation(c *gin.Context) { userID := utils.GetUserID(c) // 查找员工信息 var employee model.User if err := h.db.Where("user_id = ?", userID).First(&employee).Error; err != nil { response.BadRequest(c, "您不是员工,无法生成邀请") return } var req struct { VisitorName string `json:"visitor_name"` VisitorPhone string `json:"visitor_phone"` VisitType string `json:"visit_type"` VisitArea string `json:"visit_area"` ValidDays int `json:"valid_days"` MaxUseCount int `json:"max_use_count"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } if req.ValidDays <= 0 { req.ValidDays = 7 } if req.MaxUseCount <= 0 { req.MaxUseCount = 1 } now := time.Now() invitation := model.Invitation{ EmployeeID: employee.ID, InviteCode: utils.GenerateInviteCode(), VisitorName: req.VisitorName, VisitorPhone: req.VisitorPhone, VisitType: req.VisitType, VisitArea: req.VisitArea, ValidFrom: now, ValidUntil: now.AddDate(0, 0, req.ValidDays), MaxUseCount: req.MaxUseCount, UsedCount: 0, IsActive: true, } if err := h.db.Create(&invitation).Error; err != nil { response.InternalError(c, "创建邀请失败") return } response.SuccessWithMessage(c, "邀请创建成功", invitation) } // GetMyInvitations 我的邀请列表 // @Summary 我的邀请列表 // @Description 获取当前员工创建的邀请列表 // @Tags 邀请管理 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=[]model.Invitation} "邀请列表" // @Router /appointment/invitations/my [get] func (h *AppointmentHandler) GetMyInvitations(c *gin.Context) { userID := utils.GetUserID(c) page, pageSize := utils.ParsePagination(c) var employee model.User if err := h.db.Where("user_id = ?", userID).First(&employee).Error; err != nil { response.BadRequest(c, "您不是员工") return } var invitations []model.Invitation var total int64 query := h.db.Model(&model.Invitation{}).Where("employee_id = ?", employee.ID) query.Count(&total) query.Offset(utils.GetOffset(page, pageSize)).Limit(pageSize). Order("created_at DESC").Find(&invitations) response.SuccessPage(c, invitations, total, page, pageSize) } // ============ 公开接口 ============ // GetPublicInvitation 公开邀请详情 // @Summary 公开邀请详情 // @Description 根据邀请码获取公开的邀请详情 // @Tags 公开接口 // @Accept json // @Produce json // @Param code path string true "邀请码" // @Success 200 {object} response.Response "邀请详情" // @Router /public/invitations/{code} [get] func (h *AppointmentHandler) GetPublicInvitation(c *gin.Context) { code := c.Param("code") var invitation model.Invitation if err := h.db.Preload("Employee").Where("invite_code = ?", code).First(&invitation).Error; err != nil { response.NotFound(c, "邀请不存在或已失效") return } if !invitation.IsActive { response.BadRequest(c, "邀请已失效") return } if time.Now().After(invitation.ValidUntil) { response.BadRequest(c, "邀请已过期") return } if invitation.UsedCount >= invitation.MaxUseCount { response.BadRequest(c, "邀请使用次数已达上限") return } // InvitationVO 邀请详情视图对象 type InvitationVO struct { InviteCode string `json:"invite_code"` // 邀请码 EmployeeID uint `json:"employee_id"` // 发起邀请的员工ID EmployeeName string `json:"employee_name"` // 发起邀请的员工姓名 EmployeePhone string `json:"employee_phone"` // 发起邀请的员工电话 Department string `json:"department"` // 员工所属部门 VisitorName string `json:"visitor_name"` // 被邀请访客姓名 VisitorPhone string `json:"visitor_phone"` // 被邀请访客电话 VisitType string `json:"visit_type"` // 来访目的 VisitArea string `json:"visit_area"` // 允许访问的区域 ValidUntil string `json:"valid_until"` // 邀请失效时间 } // 查询邀请人部门信息 deptName := "" var empUD model.UserDepartment if err := h.db.Where("user_id = ?", invitation.EmployeeID).First(&empUD).Error; err == nil { deptName = empUD.DepartmentName } vo := InvitationVO{ InviteCode: invitation.InviteCode, EmployeeID: invitation.EmployeeID, EmployeeName: invitation.Employee.RealName, EmployeePhone: invitation.Employee.Phone, Department: deptName, VisitorName: invitation.VisitorName, VisitorPhone: invitation.VisitorPhone, VisitType: invitation.VisitType, VisitArea: invitation.VisitArea, ValidUntil: invitation.ValidUntil.Format("2006-01-02 15:04:05"), } response.Success(c, vo) } // ============ 保安核验 ============ // GetTodayAppointments 今日预约列表 // @Summary 今日预约列表 // @Description 保安查看今日已审核通过的预约列表 // @Tags 保安核验 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=object{stats=object{total=int,checked_in=int,checked_out=int},appointments=[]model.Appointment}} "预约列表" // @Router /guard/today-appointments [get] func (h *AppointmentHandler) GetTodayAppointments(c *gin.Context) { today := time.Now().Format("2006-01-02") startOfDay, _ := time.Parse("2006-01-02", today) endOfDay := startOfDay.Add(24 * time.Hour) var total int64 query := h.db.Model(&model.Appointment{}). Where("status = 1"). // 已通过 Where("visit_start_time >= ? AND visit_start_time < ?", startOfDay, endOfDay) query.Count(&total) // 统计已进场/已出场数量 var checkedIn, checkedOut int64 h.db.Model(&model.VisitRecord{}). Where("created_at >= ? AND created_at < ?", startOfDay, endOfDay). Where("check_type = ?", 1). Count(&checkedIn) h.db.Model(&model.VisitRecord{}). Where("created_at >= ? AND created_at < ?", startOfDay, endOfDay). Where("check_type = ?", 2). Count(&checkedOut) stats := map[string]interface{}{ "total": total, "checked_in": checkedIn, "checked_out": checkedOut, } response.Success(c, map[string]interface{}{ "stats": stats, }) } // CheckIn 登记进场 // @Summary 登记进场 // @Description 保安登记访客进场 // @Tags 保安核验 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{appointment_id=uint,remark=string} true "登记信息" // @Success 200 {object} response.Response "登记进场成功" // @Router /guard/check-in [post] func (h *AppointmentHandler) CheckIn(c *gin.Context) { h.checkRecord(c, 1) } // CheckOut 登记出场 // @Summary 登记出场 // @Description 保安登记访客出场 // @Tags 保安核验 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{appointment_id=uint,remark=string} true "登记信息" // @Success 200 {object} response.Response "登记出场成功" // @Router /guard/check-out [post] func (h *AppointmentHandler) CheckOut(c *gin.Context) { h.checkRecord(c, 2) } // GetVisitRecords 获取预约的进出场记录 // @Summary 获取进出场记录 // @Description 按预约ID查询进出场记录列表 // @Tags 保安核验 // @Accept json // @Produce json // @Security BearerAuth // @Param appointment_id query uint true "预约ID" // @Success 200 {object} response.Response{data=[]model.VisitRecord} // @Router /guard/records [get] func (h *AppointmentHandler) GetVisitRecords(c *gin.Context) { appointmentIDStr := c.Query("appointment_id") appointmentID, err := strconv.ParseUint(appointmentIDStr, 10, 64) if err != nil { response.BadRequest(c, "appointment_id 参数错误") return } var records []model.VisitRecord h.db.Where("appointment_id = ?", appointmentID). Order("check_time ASC"). Find(&records) if records == nil { records = []model.VisitRecord{} } response.Success(c, records) } func (h *AppointmentHandler) checkRecord(c *gin.Context, checkType int) { var req struct { AppointmentID uint `json:"appointment_id" binding:"required"` Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } var appointment model.Appointment if err := h.db.First(&appointment, req.AppointmentID).Error; err != nil { response.NotFound(c, "预约不存在") return } if appointment.Status != 1 { response.BadRequest(c, "预约未通过审核") return } // 出场时校验是否已进场 if checkType == 2 { var checkInCount int64 h.db.Model(&model.VisitRecord{}). Where("appointment_id = ? AND check_type = 1 AND is_valid = true", req.AppointmentID). Count(&checkInCount) if checkInCount == 0 { response.BadRequest(c, "该预约尚未进场,无法登记出场") return } } guardID := utils.GetUserID(c) record := model.VisitRecord{ AppointmentID: req.AppointmentID, GuardID: guardID, CheckType: checkType, CheckTime: time.Now(), IsValid: true, Remark: req.Remark, } if err := h.db.Create(&record).Error; err != nil { response.InternalError(c, "登记失败") return } action := "进场" if checkType == 2 { action = "出场" } response.SuccessWithMessage(c, "登记"+action+"成功", record) } // ============ 领导追溯 ============ // GetLeaderAppointments 领导查看预约(仅部门领导可查看) // @Summary 领导查看预约 // @Description 部门领导查看本部门员工的预约列表 // @Tags 领导审批 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=[]model.Appointment} "预约列表" // @Router /leader/appointments [get] func (h *AppointmentHandler) GetLeaderAppointments(c *gin.Context) { userID := utils.GetUserID(c) page, pageSize := utils.ParsePagination(c) // 查找当前用户的员工信息,校验是否为领导 var employee model.User if err := h.db.Where("user_id = ?", userID).First(&employee).Error; err != nil { response.BadRequest(c, "您不是员工") return } var empUD model.UserDepartment if err := h.db.Where("user_id = ?", employee.ID).First(&empUD).Error; err != nil { response.BadRequest(c, "您未分配部门") return } // 只有领导可以查看部门预约 isLeader := empUD.EmployeeType == "department_leader" || empUD.EmployeeType == "deputy_leader" || empUD.EmployeeType == "vice_president" || empUD.EmployeeType == "general_manager" if !isLeader { response.Forbidden(c, "仅部门领导可查看") return } // 查找同部门员工的预约 + 被指定为审批人的预约 deptID := empUD.DepartmentID var employeeIDs []uint h.db.Model(&model.UserDepartment{}). Where("department_id = ?", deptID). Pluck("user_id", &employeeIDs) var appointments []model.Appointment var total int64 query := h.db.Model(&model.Appointment{}). Where("visit_user_id IN ? OR id IN (SELECT pi.business_id FROM process_instances pi JOIN tasks t ON t.instance_id = pi.id WHERE pi.business_type = ? AND t.assignee_id = ?)", employeeIDs, "appointment", employee.ID) query.Count(&total) query.Offset(utils.GetOffset(page, pageSize)).Limit(pageSize). Order("created_at DESC").Find(&appointments) response.SuccessPage(c, appointments, total, page, pageSize) } // ============ 员工验证 ============ // VerifyEmployee 验证员工姓名+手机号 // @Summary 验证员工姓名+手机号 // @Description 根据姓名和手机号验证是否为正式员工 // @Tags 员工管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{name=string,phone=string} true "姓名和手机号" // @Success 200 {object} response.Response "员工信息" // @Router /appointment/employees/verify [post] func (h *AppointmentHandler) VerifyEmployee(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Phone string `json:"phone" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } var employee model.User if err := h.db.Where("real_name = ? AND phone = ? AND role = 'employee' AND status = 1", req.Name, req.Phone).First(&employee).Error; err != nil { response.NotFound(c, "未找到匹配的员工,请确认姓名和手机号是否正确") return } type EmployeeVO struct { ID uint `json:"id"` UserID uint `json:"user_id"` Name string `json:"name"` Department string `json:"department"` DepartmentID *uint `json:"department_id"` Phone string `json:"phone"` Position string `json:"position"` } // 查询部门信息 deptName := "" var deptID *uint var empUD model.UserDepartment if err := h.db.Where("user_id = ?", employee.ID).First(&empUD).Error; err == nil { deptName = empUD.DepartmentName deptID = &empUD.DepartmentID } response.Success(c, EmployeeVO{ ID: employee.ID, UserID: employee.ID, Name: employee.RealName, Department: deptName, DepartmentID: deptID, Phone: employee.Phone, Position: empUD.Position, }) } // ============ 访客历史 ============ // GetMyVisitors 获取当前用户的历史访客列表(按手机号去重,取最新记录) // @Summary 获取历史访客列表 // @Description 获取当前用户的常用访客列表,按手机号去重 // @Tags 访客管理 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response "访客列表" // @Router /appointment/visitors/my [get] func (h *AppointmentHandler) GetMyVisitors(c *gin.Context) { userID := utils.GetUserID(c) // 查找当前用户所有预约的访客(作为访客创建时的 user_id 查找) var visitors []model.Visitor if err := h.db.Where("user_id = ?", userID). Order("created_at DESC"). Find(&visitors).Error; err != nil { response.InternalError(c, "查询失败") return } // 按手机号去重 seen := make(map[string]bool) type VisitorVO struct { ID uint `json:"id"` Company string `json:"company"` Name string `json:"name"` Phone string `json:"phone"` Gender int `json:"gender"` IDType string `json:"id_type"` IDNumber string `json:"id_number"` } var result []VisitorVO for _, v := range visitors { if seen[v.Phone] { continue } seen[v.Phone] = true result = append(result, VisitorVO{ ID: v.ID, Company: v.Company, Name: v.Name, Phone: v.Phone, Gender: v.Gender, IDType: v.IDType, IDNumber: v.IDNumber, }) } response.Success(c, result) } // SaveVisitorTemplate 保存常用访客 // @Summary 保存常用访客 // @Description 保存或更新常用访客信息 // @Tags 访客管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{name=string,phone=string,company=string,gender=int,id_type=string,id_number=string} true "访客信息" // @Success 200 {object} response.Response "保存成功" // @Router /appointment/visitor-templates [post] func (h *AppointmentHandler) SaveVisitorTemplate(c *gin.Context) { userID := utils.GetUserID(c) var req struct { Name string `json:"name" binding:"required"` Phone string `json:"phone" binding:"required"` Company string `json:"company"` Gender int `json:"gender"` IDType string `json:"id_type"` IDNumber string `json:"id_number"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } // 先按手机号查找是否已存在 var existing model.Visitor if err := h.db.Where("phone = ? AND user_id = ?", req.Phone, userID).First(&existing).Error; err == nil { // 更新已有记录 h.db.Model(&existing).Updates(map[string]interface{}{ "name": req.Name, "company": req.Company, "gender": req.Gender, "id_type": req.IDType, "id_number": req.IDNumber, }) response.SuccessWithMessage(c, "已更新", nil) return } visitor := model.Visitor{ UserID: &userID, Name: req.Name, Phone: req.Phone, Company: req.Company, Gender: req.Gender, IDType: req.IDType, IDNumber: req.IDNumber, } if err := h.db.Create(&visitor).Error; err != nil { response.InternalError(c, "保存失败") return } response.SuccessWithMessage(c, "保存成功", nil) } // DeleteVisitorTemplate 删除常用访客 // @Summary 删除常用访客 // @Description 根据ID删除常用访客记录 // @Tags 访客管理 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "访客ID" // @Success 200 {object} response.Response "删除成功" // @Router /appointment/visitor-templates/{id} [delete] func (h *AppointmentHandler) DeleteVisitorTemplate(c *gin.Context) { userID := utils.GetUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } result := h.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.Visitor{}) if result.RowsAffected == 0 { response.NotFound(c, "记录不存在") return } response.SuccessWithMessage(c, "删除成功", nil) } // ============ 物品模板管理 ============ // ListGoodsTemplates 获取当前用户的物品模板列表 // @Summary 获取物品模板列表 // @Description 获取当前用户的物品模板列表 // @Tags 物品模板 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=[]model.GoodsTemplate} "物品模板列表" // @Router /appointment/goods-templates [get] func (h *AppointmentHandler) ListGoodsTemplates(c *gin.Context) { userID := utils.GetUserID(c) var templates []model.GoodsTemplate if err := h.db.Where("user_id = ?", userID). Order("created_at DESC"). Find(&templates).Error; err != nil { response.InternalError(c, "查询失败") return } response.Success(c, templates) } // CreateGoodsTemplate 创建物品模板 // @Summary 创建物品模板 // @Description 创建新的物品模板 // @Tags 物品模板 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{name=string,quantity=int,model=string,remark=string} true "物品模板信息" // @Success 200 {object} response.Response{data=model.GoodsTemplate} "创建成功" // @Router /appointment/goods-templates [post] func (h *AppointmentHandler) CreateGoodsTemplate(c *gin.Context) { userID := utils.GetUserID(c) var req struct { Name string `json:"name" binding:"required"` Quantity int `json:"quantity"` Model string `json:"model"` Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } if req.Quantity <= 0 { req.Quantity = 1 } template := model.GoodsTemplate{ UserID: userID, Name: req.Name, Quantity: req.Quantity, Model: req.Model, Remark: req.Remark, } if err := h.db.Create(&template).Error; err != nil { response.InternalError(c, "创建失败") return } response.SuccessWithMessage(c, "创建成功", template) } // UpdateGoodsTemplate 更新物品模板 // @Summary 更新物品模板 // @Description 根据ID更新物品模板信息 // @Tags 物品模板 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "物品模板ID" // @Param body body object{name=string,quantity=int,model=string,remark=string} true "物品模板信息" // @Success 200 {object} response.Response "更新成功" // @Router /appointment/goods-templates/{id} [put] func (h *AppointmentHandler) UpdateGoodsTemplate(c *gin.Context) { userID := utils.GetUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var template model.GoodsTemplate if err := h.db.Where("id = ? AND user_id = ?", id, userID).First(&template).Error; err != nil { response.NotFound(c, "物品模板不存在") return } var req struct { Name string `json:"name"` Quantity int `json:"quantity"` Model string `json:"model"` Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } updates := map[string]interface{}{} if req.Name != "" { updates["name"] = req.Name } if req.Quantity > 0 { updates["quantity"] = req.Quantity } updates["model"] = req.Model updates["remark"] = req.Remark if err := h.db.Model(&template).Updates(updates).Error; err != nil { response.InternalError(c, "更新失败") return } response.SuccessWithMessage(c, "更新成功", nil) } // DeleteGoodsTemplate 删除物品模板 // @Summary 删除物品模板 // @Description 根据ID删除物品模板 // @Tags 物品模板 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "物品模板ID" // @Success 200 {object} response.Response "删除成功" // @Router /appointment/goods-templates/{id} [delete] func (h *AppointmentHandler) DeleteGoodsTemplate(c *gin.Context) { userID := utils.GetUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } result := h.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.GoodsTemplate{}) if result.RowsAffected == 0 { response.NotFound(c, "物品模板不存在") return } response.SuccessWithMessage(c, "删除成功", nil) } // ============ 车辆信息管理 ============ // ListVehicleInfos 获取车辆信息列表 // @Summary 获取车辆信息列表 // @Description 获取当前用户保存的车辆信息列表 // @Tags 车辆信息 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=[]model.VehicleInfo} "车辆列表" // @Router /appointment/vehicles [get] func (h *AppointmentHandler) ListVehicleInfos(c *gin.Context) { userID := utils.GetUserID(c) var vehicles []model.VehicleInfo h.db.Where("user_id = ?", userID).Order("created_at DESC").Find(&vehicles) response.Success(c, vehicles) } // CreateVehicleInfo 创建车辆信息 // @Summary 创建车辆信息 // @Description 保存新的车辆信息到当前用户 // @Tags 车辆信息 // @Accept json // @Produce json // @Security BearerAuth // @Param body body object{license_plate=string,brand=string,color=string} true "车辆信息" // @Success 200 {object} response.Response{data=model.VehicleInfo} "创建成功" // @Router /appointment/vehicles [post] func (h *AppointmentHandler) CreateVehicleInfo(c *gin.Context) { userID := utils.GetUserID(c) var req struct { LicensePlate string `json:"license_plate" binding:"required"` Brand string `json:"brand"` Color string `json:"color"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } vehicle := model.VehicleInfo{ UserID: userID, LicensePlate: req.LicensePlate, Brand: req.Brand, Color: req.Color, } if err := h.db.Create(&vehicle).Error; err != nil { response.InternalError(c, "保存失败") return } response.SuccessWithMessage(c, "保存成功", vehicle) } // DeleteVehicleInfo 删除车辆信息 // @Summary 删除车辆信息 // @Description 根据ID删除保存的车辆信息 // @Tags 车辆信息 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "车辆信息ID" // @Success 200 {object} response.Response "删除成功" // @Router /appointment/vehicles/{id} [delete] func (h *AppointmentHandler) DeleteVehicleInfo(c *gin.Context) { userID := utils.GetUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } result := h.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.VehicleInfo{}) if result.RowsAffected == 0 { response.NotFound(c, "车辆信息不存在") return } response.SuccessWithMessage(c, "删除成功", nil) } // UpdateVehicleInfo 更新车辆信息 // @Summary 更新车辆信息 // @Description 更新已保存的车辆信息 // @Tags 车辆信息 // @Accept json // @Produce json // @Security BearerAuth // @Param id path integer true "车辆信息ID" // @Param body body object{license_plate=string,brand=string,color=string} true "车辆信息" // @Success 200 {object} response.Response "更新成功" // @Router /appointment/vehicles/{id} [put] func (h *AppointmentHandler) UpdateVehicleInfo(c *gin.Context) { userID := utils.GetUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var req struct { LicensePlate string `json:"license_plate" binding:"required"` Brand string `json:"brand"` Color string `json:"color"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } result := h.db.Model(&model.VehicleInfo{}).Where("id = ? AND user_id = ?", id, userID).Updates(map[string]interface{}{ "license_plate": req.LicensePlate, "brand": req.Brand, "color": req.Color, }) if result.RowsAffected == 0 { response.NotFound(c, "车辆信息不存在") return } response.SuccessWithMessage(c, "更新成功", nil) } // ============ 通用数据查询 ============ // ListVisitTypes 获取全部启用的来访目的(公开接口) func (h *AppointmentHandler) ListVisitTypes(c *gin.Context) { var types []model.VisitType h.db.Where("status = 1").Order("sort ASC").Find(&types) if types == nil { types = []model.VisitType{} } response.Success(c, types) } // ListVisitorAreas 获取全部启用的到访区域(公开接口) func (h *AppointmentHandler) ListVisitorAreas(c *gin.Context) { var areas []model.VisitorArea h.db.Where("status = 1").Order("sort ASC").Find(&areas) if areas == nil { areas = []model.VisitorArea{} } response.Success(c, areas) } // formatVisitTimeRange 格式化访问时间段:06月20日08:00 ~ 23:59 func formatVisitTimeRange(a *model.Appointment) string { if a.VisitStartTime.IsZero() { return "" } return a.VisitStartTime.Format("01-02 15:04") + "-" + a.VisitEndTime.Format("15:04") } // formatVisitTimeShort 格式化简短时间段:01-02 15:04 ~ 15:04 func formatVisitTimeShort(a *model.Appointment) string { if a.VisitStartTime.IsZero() { return "" } start := a.VisitStartTime.Format("01-02 15:04") endTime := a.VisitEndTime.Format("15:04") return fmt.Sprintf("%s ~ %s", start, endTime) } // RegisterRoutes 注册预约路由 func (h *AppointmentHandler) RegisterRoutes(r *gin.RouterGroup) { appt := r.Group("/appointment") { // 员工搜索 & 验证 appt.POST("/employees/search", h.SearchEmployees) appt.POST("/employees/verify", h.VerifyEmployee) // 访客历史 appt.GET("/visitors/my", h.GetMyVisitors) appt.POST("/visitor-templates", h.SaveVisitorTemplate) appt.DELETE("/visitor-templates/:id", h.DeleteVisitorTemplate) // 物品模板管理 appt.GET("/goods-templates", h.ListGoodsTemplates) appt.POST("/goods-templates", h.CreateGoodsTemplate) appt.PUT("/goods-templates/:id", h.UpdateGoodsTemplate) appt.DELETE("/goods-templates/:id", h.DeleteGoodsTemplate) // 车辆信息管理 appt.GET("/vehicles", h.ListVehicleInfos) appt.POST("/vehicles", h.CreateVehicleInfo) appt.DELETE("/vehicles/:id", h.DeleteVehicleInfo) appt.PUT("/vehicles/:id", h.UpdateVehicleInfo) // 预约管理 appt.POST("/appointments", h.CreateAppointment) appt.GET("/appointments/my", h.GetMyAppointments) appt.GET("/appointments/all", h.GetAllAppointments) appt.GET("/appointments/:id", h.GetAppointmentDetail) // 邀请管理 appt.POST("/invitations", h.CreateInvitation) appt.GET("/invitations/my", h.GetMyInvitations) } // 公开接口(无需认证) public := r.Group("/public") { public.GET("/invitations/:code", h.GetPublicInvitation) public.GET("/visit-types", h.ListVisitTypes) public.GET("/visitor-areas", h.ListVisitorAreas) } // 保安接口 guard := r.Group("/guard") { guard.GET("/today-appointments", h.GetTodayAppointments) guard.POST("/check-in", h.CheckIn) guard.POST("/check-out", h.CheckOut) guard.GET("/records", h.GetVisitRecords) } // 领导接口 leader := r.Group("/leader") { leader.GET("/appointments", h.GetLeaderAppointments) } }