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)
}
}