Files
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

1698 lines
54 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"` // 员工IDEmployee表主键
UserID uint `json:"user_id"` // 关联的用户IDUser表主键
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 += "<br>" + v.Company
}
if v.IDCard != "" {
line += "<br>" + v.IDCard
}
visitorList += line + "<br>"
}
// 从预约对象中解析车辆信息
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 + "<br>"
}
}
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)
}
}