1698 lines
54 KiB
Go
1698 lines
54 KiB
Go
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 += "<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)
|
||
}
|
||
}
|