1899 lines
54 KiB
Go
1899 lines
54 KiB
Go
package admin
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"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/pkg/wechat"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/xuri/excelize/v2"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// AdminHandler 管理员处理器
|
||
type AdminHandler struct {
|
||
db *gorm.DB
|
||
wxClient *wechat.MiniProgramClient
|
||
}
|
||
|
||
func NewAdminHandler(db *gorm.DB, wxClient *wechat.MiniProgramClient) *AdminHandler {
|
||
return &AdminHandler{db: db, wxClient: wxClient}
|
||
}
|
||
|
||
// ============ 用户管理 ============
|
||
|
||
// UserVO 用户视图对象
|
||
type UserVO struct {
|
||
ID uint `json:"id"`
|
||
Openid string `json:"openid"`
|
||
Nickname string `json:"nickname"`
|
||
RealName string `json:"real_name"`
|
||
Phone string `json:"phone"`
|
||
AvatarURL string `json:"avatar_url"`
|
||
Role string `json:"role"`
|
||
RoleName string `json:"role_name"`
|
||
Status int `json:"status"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// roleName 角色编码转中文名
|
||
func roleName(code string) string {
|
||
switch code {
|
||
case "employee":
|
||
return "员工"
|
||
case "guard":
|
||
return "保安"
|
||
default:
|
||
return "访客"
|
||
}
|
||
}
|
||
|
||
// @Summary 用户列表
|
||
// @Description 分页查询用户列表,支持关键词搜索
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param page query int false "页码" default(1)
|
||
// @Param page_size query int false "每页数量" default(10)
|
||
// @Param keyword query string false "搜索关键词(姓名/昵称/手机号)"
|
||
// @Success 200 {object} response.Response{data=response.PageData{list=[]UserVO}} "成功"
|
||
// @Router /admin/users [get]
|
||
|
||
// ListUsers 用户列表
|
||
func (h *AdminHandler) ListUsers(c *gin.Context) {
|
||
page, pageSize := utils.ParsePagination(c)
|
||
keyword := c.DefaultQuery("keyword", "")
|
||
userType := c.DefaultQuery("type", "") // employee / visitor / guard
|
||
statusStr := c.DefaultQuery("status", "")
|
||
|
||
var users []model.User
|
||
var total int64
|
||
|
||
query := h.db.Model(&model.User{})
|
||
if keyword != "" {
|
||
query = query.Where("real_name LIKE ? OR nickname LIKE ? OR phone LIKE ?",
|
||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||
}
|
||
if userType != "" {
|
||
query = query.Where("role = ?", userType)
|
||
}
|
||
if statusStr != "" {
|
||
query = query.Where("status = ?", statusStr)
|
||
}
|
||
|
||
query.Count(&total)
|
||
query.Offset(utils.GetOffset(page, pageSize)).Limit(pageSize).Order("created_at DESC, id DESC").Find(&users)
|
||
|
||
var vos []UserVO
|
||
for _, u := range users {
|
||
vos = append(vos, userToVO(&u))
|
||
}
|
||
|
||
response.SuccessPage(c, vos, total, page, pageSize)
|
||
}
|
||
|
||
// @Summary 更新用户信息
|
||
// @Description 管理员更新用户信息(昵称、姓名、手机号、状态、角色等)
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "用户ID"
|
||
// @Param body body object{real_name=string,nickname=string,phone=string,status=int,role=string} true "更新信息"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/users/{id} [put]
|
||
|
||
// UpdateUser 更新用户信息(管理员操作)
|
||
func (h *AdminHandler) UpdateUser(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
RealName string `json:"real_name"`
|
||
Nickname string `json:"nickname"`
|
||
Phone string `json:"phone"`
|
||
Status *int `json:"status"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
updates := map[string]interface{}{}
|
||
if req.RealName != "" {
|
||
updates["real_name"] = req.RealName
|
||
}
|
||
if req.Nickname != "" {
|
||
updates["nickname"] = req.Nickname
|
||
}
|
||
if req.Phone != "" {
|
||
updates["phone"] = req.Phone
|
||
}
|
||
if req.Status != nil {
|
||
updates["status"] = *req.Status
|
||
}
|
||
if req.Role != "" {
|
||
updates["role"] = req.Role
|
||
}
|
||
|
||
if err := h.db.Model(&model.User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
|
||
// 如果设为员工角色且尚无 Employee 记录,自动创建
|
||
if req.Role == "employee" {
|
||
var count int64
|
||
h.db.Model(&model.UserDepartment{}).Where("user_id = ?", id).Count(&count)
|
||
if count == 0 {
|
||
h.db.Model(&model.User{}).Where("id = ?", id).Update("role", "employee")
|
||
// 同时创建 UserDepartment 记录
|
||
h.db.Create(&model.UserDepartment{
|
||
UserID: uint(id),
|
||
EmployeeStatus: 1,
|
||
EmployeeType: "employee",
|
||
})
|
||
} else {
|
||
h.db.Model(&model.User{}).Where("id = ?", id).Update("role", "employee")
|
||
h.db.Model(&model.UserDepartment{}).Where("user_id = ?", id).Update("employee_status", 1)
|
||
}
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
func userToVO(u *model.User) UserVO {
|
||
code := u.Role
|
||
if code == "" {
|
||
code = "visitor"
|
||
}
|
||
return UserVO{
|
||
ID: u.ID,
|
||
Openid: u.Openid,
|
||
Nickname: u.Nickname,
|
||
RealName: u.RealName,
|
||
Phone: u.Phone,
|
||
AvatarURL: u.AvatarURL,
|
||
Status: u.Status,
|
||
Role: code,
|
||
RoleName: roleName(code),
|
||
CreatedAt: u.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
}
|
||
|
||
// RegisterRoutes 注册管理路由
|
||
func (h *AdminHandler) RegisterRoutes(r *gin.RouterGroup, jwtMiddleware gin.HandlerFunc) {
|
||
admin := r.Group("/admin")
|
||
admin.Use(jwtMiddleware)
|
||
{
|
||
// 用户管理
|
||
admin.GET("/users", h.ListUsers)
|
||
admin.PUT("/users/:id", h.UpdateUser)
|
||
|
||
// Banner 管理
|
||
admin.GET("/banners", h.ListBanners)
|
||
|
||
// 员工列表(审批配置用)
|
||
admin.GET("/employees", h.ListEmployees)
|
||
admin.POST("/banners", h.CreateBanner)
|
||
admin.PUT("/banners/:id", h.UpdateBanner)
|
||
admin.DELETE("/banners/:id", h.DeleteBanner)
|
||
|
||
// 通知管理
|
||
admin.GET("/notices", h.ListNotices)
|
||
admin.POST("/notices", h.CreateNotice)
|
||
admin.PUT("/notices/:id", h.UpdateNotice)
|
||
admin.DELETE("/notices/:id", h.DeleteNotice)
|
||
|
||
// 组织架构(部门树)
|
||
admin.GET("/departments", h.ListDepartments)
|
||
|
||
// 员工认证审核
|
||
admin.GET("/certifications", h.ListCertifications)
|
||
admin.PUT("/certifications/:id/audit", h.AuditCertification)
|
||
|
||
// 部门管理(树形结构)
|
||
admin.GET("/department-tree", h.ListDepartmentTree)
|
||
admin.GET("/departments-all", h.ListAllDepartments)
|
||
admin.POST("/departments", h.CreateDepartment)
|
||
admin.PUT("/departments/:id", h.UpdateDepartment)
|
||
admin.DELETE("/departments/:id", h.DeleteDepartment)
|
||
|
||
// 员工-部门关联
|
||
admin.PUT("/employees/:userId/department", h.SetEmployeeDepartment)
|
||
admin.DELETE("/employees/:userId/department", h.ClearEmployeeDepartment)
|
||
admin.GET("/employee-departments", h.ListEmployeeDepartments)
|
||
|
||
// 到访区域管理
|
||
admin.GET("/visitor-areas", h.ListVisitorAreas)
|
||
admin.POST("/visitor-areas", h.CreateVisitorArea)
|
||
admin.PUT("/visitor-areas/:id", h.UpdateVisitorArea)
|
||
admin.DELETE("/visitor-areas/:id", h.DeleteVisitorArea)
|
||
|
||
// 来访目的管理
|
||
admin.GET("/visit-types", h.ListVisitTypes)
|
||
admin.POST("/visit-types", h.CreateVisitType)
|
||
admin.PUT("/visit-types/:id", h.UpdateVisitType)
|
||
admin.DELETE("/visit-types/:id", h.DeleteVisitType)
|
||
|
||
// 预约管理(仅查看,审核走工作流引擎)
|
||
admin.GET("/appointments", h.ListAppointments)
|
||
|
||
// 审批流配置:部门分管领导(存储在 department.vp_user_ids)
|
||
admin.PUT("/departments/:id/vp-users", h.SetDepartmentVPs)
|
||
|
||
// 审批流配置:设置部门指定审批人
|
||
admin.PUT("/departments/:id/approver", h.SetDepartmentApprover)
|
||
|
||
// 待确认员工管理(通讯录导入)
|
||
admin.POST("/employees/import", h.ImportPendingEmployees)
|
||
admin.POST("/employees/import-excel", h.ImportPendingEmployeesExcel)
|
||
admin.GET("/employees/pending", h.ListPendingEmployees)
|
||
admin.GET("/employees/pending/stats", h.GetPendingEmployeeStats)
|
||
admin.POST("/employees/pending/:id/match", h.MatchPendingEmployee)
|
||
admin.POST("/employees/pending/sync-all", h.SyncAllPendingEmployees)
|
||
|
||
// 全局兜底审批人
|
||
admin.GET("/global-approvers", h.ListGlobalApprovers)
|
||
admin.POST("/global-approvers", h.CreateGlobalApprover)
|
||
admin.DELETE("/global-approvers/:id", h.DeleteGlobalApprover)
|
||
}
|
||
}
|
||
|
||
// ============ Banner 管理 ============
|
||
|
||
// @Summary Banner列表
|
||
// @Description 获取所有Banner列表,按排序字段升序
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]model.Banner} "成功"
|
||
// @Router /admin/banners [get]
|
||
|
||
// ListBanners Banner 列表
|
||
func (h *AdminHandler) ListBanners(c *gin.Context) {
|
||
var banners []model.Banner
|
||
h.db.Order("sort ASC").Find(&banners)
|
||
response.Success(c, banners)
|
||
}
|
||
|
||
// @Summary 创建Banner
|
||
// @Description 创建一个新的Banner
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param body body object{image_url=string,jump_path=string,is_jump=bool,sort=int} true "Banner信息"
|
||
// @Success 200 {object} response.Response{data=model.Banner} "成功"
|
||
// @Router /admin/banners [post]
|
||
|
||
// CreateBanner 创建 Banner
|
||
func (h *AdminHandler) CreateBanner(c *gin.Context) {
|
||
var req struct {
|
||
ImageURL string `json:"image_url" binding:"required"`
|
||
JumpPath string `json:"jump_path"`
|
||
IsJump bool `json:"is_jump"`
|
||
Sort int `json:"sort"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
banner := model.Banner{
|
||
ImageURL: req.ImageURL,
|
||
JumpPath: req.JumpPath,
|
||
IsJump: req.IsJump,
|
||
Sort: req.Sort,
|
||
IsValid: true,
|
||
}
|
||
if err := h.db.Create(&banner).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "创建成功", banner)
|
||
}
|
||
|
||
// @Summary 更新Banner
|
||
// @Description 更新指定Banner的信息
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "Banner ID"
|
||
// @Param body body object{image_url=string,jump_path=string,is_jump=bool,sort=int,is_valid=bool} true "更新信息"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/banners/{id} [put]
|
||
|
||
// UpdateBanner 更新 Banner
|
||
func (h *AdminHandler) UpdateBanner(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
var banner model.Banner
|
||
if err := h.db.First(&banner, id).Error; err != nil {
|
||
response.NotFound(c, "Banner 不存在")
|
||
return
|
||
}
|
||
var req struct {
|
||
ImageURL string `json:"image_url"`
|
||
JumpPath string `json:"jump_path"`
|
||
IsJump *bool `json:"is_jump"`
|
||
Sort *int `json:"sort"`
|
||
IsValid *bool `json:"is_valid"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
updates := map[string]interface{}{}
|
||
if req.ImageURL != "" {
|
||
updates["image_url"] = req.ImageURL
|
||
}
|
||
if req.JumpPath != "" {
|
||
updates["jump_path"] = req.JumpPath
|
||
}
|
||
if req.IsJump != nil {
|
||
updates["is_jump"] = *req.IsJump
|
||
}
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
if req.IsValid != nil {
|
||
updates["is_valid"] = *req.IsValid
|
||
}
|
||
if err := h.db.Model(&banner).Updates(updates).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// @Summary 删除Banner
|
||
// @Description 删除指定Banner
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "Banner ID"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/banners/{id} [delete]
|
||
|
||
// DeleteBanner 删除 Banner
|
||
func (h *AdminHandler) DeleteBanner(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
if err := h.db.Delete(&model.Banner{}, id).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ============ 通知管理 ============
|
||
|
||
// @Summary 通知列表
|
||
// @Description 获取所有通知列表,按排序字段升序
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]model.Notice} "成功"
|
||
// @Router /admin/notices [get]
|
||
|
||
// ListNotices 通知列表
|
||
func (h *AdminHandler) ListNotices(c *gin.Context) {
|
||
var notices []model.Notice
|
||
h.db.Order("sort ASC").Find(¬ices)
|
||
response.Success(c, notices)
|
||
}
|
||
|
||
// @Summary 创建通知
|
||
// @Description 创建一个新的通知
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param body body object{title=string,content=string,sort=int} true "通知信息"
|
||
// @Success 200 {object} response.Response{data=model.Notice} "成功"
|
||
// @Router /admin/notices [post]
|
||
|
||
// CreateNotice 创建通知
|
||
func (h *AdminHandler) CreateNotice(c *gin.Context) {
|
||
var req struct {
|
||
Title string `json:"title" binding:"required"`
|
||
Content string `json:"content" binding:"required"`
|
||
Sort int `json:"sort"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
notice := model.Notice{
|
||
Title: req.Title,
|
||
Content: req.Content,
|
||
Sort: req.Sort,
|
||
IsValid: true,
|
||
}
|
||
if err := h.db.Create(¬ice).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "创建成功", notice)
|
||
}
|
||
|
||
// @Summary 更新通知
|
||
// @Description 更新指定通知的信息
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "通知ID"
|
||
// @Param body body object{title=string,content=string,sort=int,is_valid=bool} true "更新信息"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/notices/{id} [put]
|
||
|
||
// UpdateNotice 更新通知
|
||
func (h *AdminHandler) UpdateNotice(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
var notice model.Notice
|
||
if err := h.db.First(¬ice, id).Error; err != nil {
|
||
response.NotFound(c, "通知不存在")
|
||
return
|
||
}
|
||
var req struct {
|
||
Title string `json:"title"`
|
||
Content string `json:"content"`
|
||
Sort *int `json:"sort"`
|
||
IsValid *bool `json:"is_valid"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
updates := map[string]interface{}{}
|
||
if req.Title != "" {
|
||
updates["title"] = req.Title
|
||
}
|
||
if req.Content != "" {
|
||
updates["content"] = req.Content
|
||
}
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
if req.IsValid != nil {
|
||
updates["is_valid"] = *req.IsValid
|
||
}
|
||
if err := h.db.Model(¬ice).Updates(updates).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// @Summary 删除通知
|
||
// @Description 删除指定通知
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "通知ID"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/notices/{id} [delete]
|
||
|
||
// DeleteNotice 删除通知
|
||
func (h *AdminHandler) DeleteNotice(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
if err := h.db.Delete(&model.Notice{}, id).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ============ 员工列表 ============
|
||
|
||
// @Summary 员工列表
|
||
// @Description 获取所有员工列表(供审批配置选择用),包含正式员工和role为employee的用户
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]object{id=uint,user_id=uint,name=string,department_name=string}} "成功"
|
||
// @Router /admin/employees [get]
|
||
|
||
// ListEmployees 员工列表(供审批配置选择用)
|
||
func (h *AdminHandler) ListEmployees(c *gin.Context) {
|
||
type EmployeeItem struct {
|
||
ID uint `json:"id"`
|
||
UserID uint `json:"user_id"`
|
||
Name string `json:"name"`
|
||
DepartmentName string `json:"department_name"`
|
||
Position string `json:"position"`
|
||
EmployeeType string `json:"employee_type"`
|
||
}
|
||
|
||
var items []EmployeeItem
|
||
|
||
// 查询所有状态正常的 role=employee 用户,左连接 user_departments 获取部门信息
|
||
var rows []struct {
|
||
model.User
|
||
DeptName string `gorm:"column:dept_name"`
|
||
DeptPosition string `gorm:"column:dept_position"`
|
||
DeptType string `gorm:"column:dept_employee_type"`
|
||
}
|
||
h.db.Table("users").
|
||
Select("users.*, COALESCE(ud.department_name, '') as dept_name, COALESCE(ud.position, '') as dept_position, COALESCE(ud.employee_type, 'employee') as dept_employee_type").
|
||
Joins("LEFT JOIN user_departments ud ON ud.user_id = users.id").
|
||
Where("users.role = ? AND users.status = ?", "employee", 1).
|
||
Order("users.real_name ASC").
|
||
Scan(&rows)
|
||
|
||
employeeUserIDs := make(map[uint]bool)
|
||
for _, r := range rows {
|
||
employeeUserIDs[r.User.ID] = true
|
||
name := r.User.RealName
|
||
if name == "" {
|
||
name = r.User.RealName
|
||
}
|
||
items = append(items, EmployeeItem{
|
||
ID: r.User.ID,
|
||
UserID: r.User.ID,
|
||
Name: name,
|
||
DepartmentName: r.DeptName,
|
||
Position: r.DeptPosition,
|
||
EmployeeType: r.DeptType,
|
||
})
|
||
}
|
||
|
||
// 补充 role='employee' 但尚未有关联记录的用户(兼容旧数据)
|
||
var users []model.User
|
||
h.db.Where("role = ? AND status = ?", "employee", 1).Find(&users)
|
||
for _, u := range users {
|
||
if !employeeUserIDs[u.ID] {
|
||
items = append(items, EmployeeItem{
|
||
ID: u.ID,
|
||
UserID: u.ID,
|
||
Name: u.RealName,
|
||
DepartmentName: "",
|
||
Position: "",
|
||
EmployeeType: "employee",
|
||
})
|
||
}
|
||
}
|
||
|
||
if items == nil {
|
||
items = []EmployeeItem{}
|
||
}
|
||
response.Success(c, items)
|
||
}
|
||
|
||
// ============ 组织架构 ============
|
||
|
||
// DepartmentVO 部门视图
|
||
type DepartmentVO struct {
|
||
Name string `json:"name"`
|
||
Employees []EmployeeBrief `json:"employees"`
|
||
}
|
||
|
||
// EmployeeBrief 员工简要信息
|
||
type EmployeeBrief struct {
|
||
ID uint `json:"id"`
|
||
UserID uint `json:"user_id"`
|
||
Name string `json:"name"`
|
||
Phone string `json:"phone"`
|
||
Position string `json:"position"`
|
||
EmployeeType string `json:"employee_type"`
|
||
}
|
||
|
||
// @Summary 组织架构
|
||
// @Description 获取组织架构列表,按部门分组显示员工
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]DepartmentVO} "成功"
|
||
// @Router /admin/departments [get]
|
||
|
||
// ListDepartments 获取组织架构(按部门分组)
|
||
func (h *AdminHandler) ListDepartments(c *gin.Context) {
|
||
type EmpRow struct {
|
||
model.User
|
||
DeptName string `gorm:"column:dept_name"`
|
||
DeptPosition string `gorm:"column:dept_position"`
|
||
DeptType string `gorm:"column:dept_employee_type"`
|
||
}
|
||
var rows []EmpRow
|
||
h.db.Table("users").
|
||
Select("users.*, COALESCE(ud.department_name, '') as dept_name, COALESCE(ud.position, '') as dept_position, COALESCE(ud.employee_type, 'employee') as dept_employee_type").
|
||
Joins("LEFT JOIN user_departments ud ON ud.user_id = users.id").
|
||
Where("users.status = ?", 1).
|
||
Order("dept_name ASC, users.real_name ASC").
|
||
Scan(&rows)
|
||
|
||
depMap := make(map[string][]EmployeeBrief)
|
||
for _, r := range rows {
|
||
dept := r.DeptName
|
||
if dept == "" {
|
||
dept = "未分配部门"
|
||
}
|
||
depMap[dept] = append(depMap[dept], EmployeeBrief{
|
||
ID: r.User.ID,
|
||
UserID: r.User.ID,
|
||
Name: r.User.RealName,
|
||
Phone: r.User.Phone,
|
||
Position: r.DeptPosition,
|
||
EmployeeType: r.DeptType,
|
||
})
|
||
}
|
||
|
||
var depts []DepartmentVO
|
||
for name, emps := range depMap {
|
||
depts = append(depts, DepartmentVO{Name: name, Employees: emps})
|
||
}
|
||
|
||
response.Success(c, depts)
|
||
}
|
||
|
||
// ============ 员工认证审核 ============
|
||
|
||
// @Summary 员工认证申请列表
|
||
// @Description 分页查询员工认证申请列表,支持按状态筛选和关键词搜索
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param page query int false "页码" default(1)
|
||
// @Param page_size query int false "每页数量" default(10)
|
||
// @Param status query int false "审核状态(0待审核 1通过 2拒绝)"
|
||
// @Param keyword query string false "搜索关键词"
|
||
// @Success 200 {object} response.Response{data=response.PageData{list=[]model.EmployeeCertification}} "成功"
|
||
// @Router /admin/certifications [get]
|
||
|
||
// ListCertifications 员工认证申请列表
|
||
func (h *AdminHandler) ListCertifications(c *gin.Context) {
|
||
page, pageSize := utils.ParsePagination(c)
|
||
statusStr := c.DefaultQuery("status", "")
|
||
keyword := c.Query("keyword")
|
||
|
||
var certs []model.EmployeeCertification
|
||
var total int64
|
||
|
||
query := h.db.Model(&model.EmployeeCertification{}).
|
||
Preload("User").
|
||
Preload("Department").
|
||
Preload("Approver")
|
||
if statusStr != "" {
|
||
status, _ := strconv.Atoi(statusStr)
|
||
query = query.Where("status = ?", status)
|
||
}
|
||
if keyword != "" {
|
||
query = query.Where("real_name LIKE ? OR phone LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||
}
|
||
|
||
query.Count(&total)
|
||
query.Offset(utils.GetOffset(page, pageSize)).Limit(pageSize).Order("created_at DESC").Find(&certs)
|
||
|
||
response.SuccessPage(c, certs, total, page, pageSize)
|
||
}
|
||
|
||
// @Summary 审核员工认证
|
||
// @Description 审核员工认证申请(通过/拒绝),通过时需分配部门
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "认证申请ID"
|
||
// @Param body body object{status=int,department_id=uint,comment=string} true "审核信息(status:1通过 2拒绝)"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/certifications/{id}/audit [put]
|
||
|
||
// AuditCertification 审核员工认证申请
|
||
func (h *AdminHandler) AuditCertification(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Status int `json:"status" binding:"required"` // 1 通过 2 拒绝
|
||
RealName string `json:"real_name"`
|
||
Phone string `json:"phone"`
|
||
DepartmentID *uint `json:"department_id"`
|
||
Position string `json:"position"`
|
||
Role string `json:"role"`
|
||
Comment string `json:"comment"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
if req.Status != 1 && req.Status != 2 {
|
||
response.BadRequest(c, "状态值无效")
|
||
return
|
||
}
|
||
|
||
var cert model.EmployeeCertification
|
||
if err := h.db.First(&cert, id).Error; err != nil {
|
||
response.NotFound(c, "申请不存在")
|
||
return
|
||
}
|
||
|
||
if cert.Status != 0 {
|
||
response.BadRequest(c, "该申请已处理")
|
||
return
|
||
}
|
||
|
||
approverID := utils.GetUserID(c)
|
||
|
||
realName := req.RealName
|
||
if realName == "" {
|
||
realName = cert.RealName
|
||
}
|
||
phone := req.Phone
|
||
if phone == "" {
|
||
phone = cert.Phone
|
||
}
|
||
|
||
updates := map[string]interface{}{
|
||
"status": req.Status,
|
||
"approver_id": approverID,
|
||
"real_name": realName,
|
||
"phone": phone,
|
||
"comment": req.Comment,
|
||
}
|
||
|
||
// 如果通过,设置分配的部门、职位、角色类型
|
||
if req.Status == 1 {
|
||
if req.DepartmentID == nil {
|
||
response.BadRequest(c, "通过时必须分配部门")
|
||
return
|
||
}
|
||
var dept model.Department
|
||
if err := h.db.First(&dept, *req.DepartmentID).Error; err != nil {
|
||
response.BadRequest(c, "部门不存在")
|
||
return
|
||
}
|
||
updates["department_id"] = *req.DepartmentID
|
||
updates["position"] = req.Position
|
||
updates["role"] = req.Role
|
||
}
|
||
|
||
if err := h.db.Model(&cert).Updates(updates).Error; err != nil {
|
||
response.InternalError(c, "审核失败")
|
||
return
|
||
}
|
||
|
||
// 如果通过,同步用户表 + 员工-部门关联表
|
||
if req.Status == 1 {
|
||
// 用户角色取认证申请时的角色类型(employee / guard)
|
||
userRole := cert.Role
|
||
if userRole == "" {
|
||
userRole = "employee"
|
||
}
|
||
h.db.Model(&model.User{}).Where("id = ?", cert.UserID).Updates(map[string]interface{}{
|
||
"role": userRole,
|
||
"real_name": realName,
|
||
"phone": phone,
|
||
})
|
||
|
||
// 同步员工-部门关联表(含职位、员工类型、状态)
|
||
pos := req.Position
|
||
if pos == "" {
|
||
pos = cert.Position
|
||
}
|
||
empType := req.Role
|
||
if empType == "" {
|
||
empType = "employee"
|
||
}
|
||
// 查找通讯录中的钉钉UserId(如已导入)
|
||
var pendingEmp model.PendingEmployee
|
||
dingTalkUserID := ""
|
||
if err := h.db.Where("phone = ?", phone).First(&pendingEmp).Error; err == nil {
|
||
dingTalkUserID = pendingEmp.DingTalkUserID
|
||
}
|
||
|
||
udData := map[string]interface{}{
|
||
"position": pos,
|
||
"employee_type": empType,
|
||
"employee_status": 1,
|
||
"ding_talk_user_id": dingTalkUserID,
|
||
}
|
||
if req.DepartmentID != nil {
|
||
var dept model.Department
|
||
if err := h.db.First(&dept, *req.DepartmentID).Error; err == nil {
|
||
udData["department_id"] = *req.DepartmentID
|
||
udData["department_name"] = dept.Name
|
||
}
|
||
}
|
||
var ud model.UserDepartment
|
||
result := h.db.Where("user_id = ?", cert.UserID).First(&ud)
|
||
if result.Error != nil {
|
||
udData["user_id"] = cert.UserID
|
||
h.db.Model(&model.UserDepartment{}).Create(udData)
|
||
} else {
|
||
h.db.Model(&ud).Updates(udData)
|
||
}
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "审核完成", nil)
|
||
}
|
||
|
||
// ============ 部门管理(树形结构)============
|
||
|
||
// DepartmentTreeNode 部门树节点
|
||
type DepartmentTreeNode struct {
|
||
model.Department
|
||
Children []*DepartmentTreeNode `json:"children"`
|
||
}
|
||
|
||
// buildDeptTree 构建部门树
|
||
func buildDeptTree(departments []model.Department, parentID *uint) []*DepartmentTreeNode {
|
||
var nodes []*DepartmentTreeNode
|
||
for _, d := range departments {
|
||
if (parentID == nil && d.ParentID == nil) ||
|
||
(parentID != nil && d.ParentID != nil && *d.ParentID == *parentID) {
|
||
node := &DepartmentTreeNode{
|
||
Department: d,
|
||
Children: buildDeptTree(departments, &d.ID),
|
||
}
|
||
nodes = append(nodes, node)
|
||
}
|
||
}
|
||
return nodes
|
||
}
|
||
|
||
// @Summary 部门树
|
||
// @Description 获取部门树形结构
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]DepartmentTreeNode} "成功"
|
||
// @Router /admin/department-tree [get]
|
||
|
||
// ListDepartmentTree 获取部门树
|
||
func (h *AdminHandler) ListDepartmentTree(c *gin.Context) {
|
||
var departments []model.Department
|
||
h.db.Where("status = ?", 1).Order("sort ASC, id ASC").Find(&departments)
|
||
|
||
tree := buildDeptTree(departments, nil)
|
||
response.Success(c, tree)
|
||
}
|
||
|
||
// DepartmentItem 部门扁平列表项
|
||
type DepartmentItem struct {
|
||
ID uint `json:"id"`
|
||
Name string `json:"name"`
|
||
ParentID *uint `json:"parent_id"`
|
||
LeaderName string `json:"leader_name"`
|
||
LeaderPhone string `json:"leader_phone"`
|
||
ApproverUserIDs string `json:"approver_user_ids"`
|
||
VpUserIDs string `json:"vp_user_ids"`
|
||
Sort int `json:"sort"`
|
||
Status int `json:"status"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// @Summary 所有部门列表
|
||
// @Description 获取所有部门扁平列表
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]DepartmentItem} "成功"
|
||
// @Router /admin/departments-all [get]
|
||
|
||
// ListAllDepartments 获取所有部门(扁平列表)
|
||
func (h *AdminHandler) ListAllDepartments(c *gin.Context) {
|
||
var departments []model.Department
|
||
h.db.Where("status = 1").Preload("Leader").Order("sort ASC, id ASC").Find(&departments)
|
||
|
||
var items []DepartmentItem
|
||
for _, d := range departments {
|
||
item := DepartmentItem{
|
||
ID: d.ID,
|
||
Name: d.Name,
|
||
ParentID: d.ParentID,
|
||
Sort: d.Sort,
|
||
Status: d.Status,
|
||
CreatedAt: d.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
if d.Leader != nil {
|
||
item.LeaderName = d.Leader.RealName
|
||
item.LeaderPhone = d.Leader.Phone
|
||
}
|
||
item.ApproverUserIDs = d.ApproverUserIDs
|
||
item.VpUserIDs = d.VpUserIDs
|
||
items = append(items, item)
|
||
}
|
||
|
||
response.Success(c, items)
|
||
}
|
||
|
||
// @Summary 创建部门
|
||
// @Description 创建一个新的部门
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param body body object{name=string,parent_id=uint,leader_id=uint,deputy_id=uint,sort=int,description=string} true "部门信息"
|
||
// @Success 200 {object} response.Response{data=model.Department} "成功"
|
||
// @Router /admin/departments [post]
|
||
|
||
// CreateDepartment 创建部门
|
||
func (h *AdminHandler) CreateDepartment(c *gin.Context) {
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
ParentID *uint `json:"parent_id"`
|
||
LeaderID *uint `json:"leader_id"`
|
||
DeputyID *uint `json:"deputy_id"`
|
||
Sort int `json:"sort"`
|
||
Description string `json:"description"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
// 如果指定了上级部门,检查是否存在
|
||
if req.ParentID != nil {
|
||
var parent model.Department
|
||
if err := h.db.First(&parent, *req.ParentID).Error; err != nil {
|
||
response.BadRequest(c, "上级部门不存在")
|
||
return
|
||
}
|
||
}
|
||
|
||
dept := model.Department{
|
||
Name: req.Name,
|
||
ParentID: req.ParentID,
|
||
LeaderID: req.LeaderID,
|
||
DeputyID: req.DeputyID,
|
||
Sort: req.Sort,
|
||
Status: 1,
|
||
Description: req.Description,
|
||
}
|
||
|
||
if err := h.db.Create(&dept).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "创建成功", dept)
|
||
}
|
||
|
||
// @Summary 更新部门
|
||
// @Description 更新指定部门的信息
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "部门ID"
|
||
// @Param body body object{name=string,parent_id=uint,leader_id=uint,deputy_id=uint,sort=int,status=int,description=string} true "更新信息"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/departments/{id} [put]
|
||
|
||
// UpdateDepartment 更新部门
|
||
func (h *AdminHandler) UpdateDepartment(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var dept model.Department
|
||
if err := h.db.First(&dept, id).Error; err != nil {
|
||
response.NotFound(c, "部门不存在")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Name string `json:"name"`
|
||
ParentID *uint `json:"parent_id"`
|
||
LeaderID *uint `json:"leader_id"`
|
||
DeputyID *uint `json:"deputy_id"`
|
||
Sort *int `json:"sort"`
|
||
Status *int `json:"status"`
|
||
Description string `json:"description"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
// 不能将自己的 parent_id 设为自己
|
||
if req.ParentID != nil && *req.ParentID == dept.ID {
|
||
response.BadRequest(c, "上级部门不能是自己")
|
||
return
|
||
}
|
||
|
||
updates := map[string]interface{}{}
|
||
if req.Name != "" {
|
||
updates["name"] = req.Name
|
||
}
|
||
if req.ParentID != nil {
|
||
updates["parent_id"] = *req.ParentID
|
||
}
|
||
if req.LeaderID != nil {
|
||
updates["leader_id"] = *req.LeaderID
|
||
}
|
||
if req.DeputyID != nil {
|
||
updates["deputy_id"] = *req.DeputyID
|
||
}
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
if req.Status != nil {
|
||
updates["status"] = *req.Status
|
||
}
|
||
if req.Description != "" {
|
||
updates["description"] = req.Description
|
||
}
|
||
|
||
if err := h.db.Model(&dept).Updates(updates).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// @Summary 删除部门
|
||
// @Description 删除指定部门及其关联检查
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param id path int true "部门ID"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/departments/{id} [delete]
|
||
|
||
// DeleteDepartment 删除部门
|
||
func (h *AdminHandler) DeleteDepartment(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var dept model.Department
|
||
if err := h.db.First(&dept, id).Error; err != nil {
|
||
response.NotFound(c, "部门不存在")
|
||
return
|
||
}
|
||
|
||
// 检查是否有子部门
|
||
var childCount int64
|
||
h.db.Model(&model.Department{}).Where("parent_id = ?", id).Count(&childCount)
|
||
if childCount > 0 {
|
||
response.BadRequest(c, "该部门下存在子部门,请先删除子部门")
|
||
return
|
||
}
|
||
|
||
// 检查是否有员工关联
|
||
var empCount int64
|
||
h.db.Model(&model.EmployeeCertification{}).Where("department_id = ? AND status = 1", id).Count(&empCount)
|
||
if empCount > 0 {
|
||
response.BadRequest(c, "该部门下有认证员工,请先移除关联")
|
||
return
|
||
}
|
||
|
||
if err := h.db.Delete(&dept).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ============ 员工-部门关联 ============
|
||
|
||
// @Summary 设置员工部门
|
||
// @Description 为指定员工设置所属部门
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param userId path int true "用户ID"
|
||
// @Param body body object{department_id=uint} true "部门ID"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/employees/{userId}/department [put]
|
||
|
||
// SetEmployeeDepartment 设置员工部门
|
||
func (h *AdminHandler) SetEmployeeDepartment(c *gin.Context) {
|
||
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
DepartmentID uint `json:"department_id" binding:"required"`
|
||
Position string `json:"position"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
// 检查用户是否存在
|
||
var user model.User
|
||
if err := h.db.First(&user, userID).Error; err != nil {
|
||
response.NotFound(c, "用户不存在")
|
||
return
|
||
}
|
||
|
||
// 检查部门是否存在
|
||
var dept model.Department
|
||
if err := h.db.First(&dept, req.DepartmentID).Error; err != nil {
|
||
response.NotFound(c, "部门不存在")
|
||
return
|
||
}
|
||
|
||
// 使用 UserDepartment 关联,有则更新,无则创建
|
||
var ud model.UserDepartment
|
||
result := h.db.Where("user_id = ?", userID).First(&ud)
|
||
fields := map[string]interface{}{
|
||
"department_id": req.DepartmentID,
|
||
"department_name": dept.Name,
|
||
"position": req.Position,
|
||
}
|
||
if result.Error != nil {
|
||
// 创建新关联
|
||
ud = model.UserDepartment{
|
||
UserID: uint(userID),
|
||
DepartmentID: req.DepartmentID,
|
||
DepartmentName: dept.Name,
|
||
Position: req.Position,
|
||
}
|
||
if err := h.db.Create(&ud).Error; err != nil {
|
||
response.InternalError(c, "设置部门失败")
|
||
return
|
||
}
|
||
} else {
|
||
// 更新已有关联
|
||
if err := h.db.Model(&ud).Updates(fields).Error; err != nil {
|
||
response.InternalError(c, "设置部门失败")
|
||
return
|
||
}
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "设置成功", nil)
|
||
}
|
||
|
||
// @Summary 清除员工部门
|
||
// @Description 移除指定员工的部门归属
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param userId path int true "用户ID"
|
||
// @Success 200 {object} response.Response "成功"
|
||
// @Router /admin/employees/{userId}/department [delete]
|
||
|
||
// ClearEmployeeDepartment 清除员工部门
|
||
func (h *AdminHandler) ClearEmployeeDepartment(c *gin.Context) {
|
||
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
if err := h.db.Where("user_id = ?", userID).Delete(&model.UserDepartment{}).Error; err != nil {
|
||
response.InternalError(c, "清除部门失败")
|
||
return
|
||
}
|
||
|
||
response.SuccessWithMessage(c, "已移除", nil)
|
||
}
|
||
|
||
// @Summary 员工-部门关联列表
|
||
// @Description 获取所有员工-部门关联关系
|
||
// @Tags 管理员
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Success 200 {object} response.Response{data=[]model.UserDepartment} "成功"
|
||
// @Router /admin/employee-departments [get]
|
||
|
||
// ListEmployeeDepartments 员工-部门关联列表
|
||
func (h *AdminHandler) ListEmployeeDepartments(c *gin.Context) {
|
||
var list []model.UserDepartment
|
||
h.db.Preload("Department").Find(&list)
|
||
if list == nil {
|
||
list = []model.UserDepartment{}
|
||
}
|
||
response.Success(c, list)
|
||
}
|
||
|
||
// ============ 到访区域管理 ============
|
||
|
||
// ListVisitorAreas 到访区域列表
|
||
func (h *AdminHandler) ListVisitorAreas(c *gin.Context) {
|
||
var areas []model.VisitorArea
|
||
h.db.Order("sort ASC, id ASC").Find(&areas)
|
||
if areas == nil {
|
||
areas = []model.VisitorArea{}
|
||
}
|
||
response.Success(c, areas)
|
||
}
|
||
|
||
// CreateVisitorArea 创建到访区域
|
||
func (h *AdminHandler) CreateVisitorArea(c *gin.Context) {
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
Sort int `json:"sort"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
area := model.VisitorArea{Name: req.Name, Sort: req.Sort, Status: 1}
|
||
if err := h.db.Create(&area).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "创建成功", area)
|
||
}
|
||
|
||
// UpdateVisitorArea 更新到访区域
|
||
func (h *AdminHandler) UpdateVisitorArea(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
var area model.VisitorArea
|
||
if err := h.db.First(&area, id).Error; err != nil {
|
||
response.NotFound(c, "区域不存在")
|
||
return
|
||
}
|
||
var req struct {
|
||
Name *string `json:"name"`
|
||
Sort *int `json:"sort"`
|
||
Status *int `json:"status"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
updates := map[string]interface{}{}
|
||
if req.Name != nil {
|
||
updates["name"] = *req.Name
|
||
}
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
if req.Status != nil {
|
||
updates["status"] = *req.Status
|
||
}
|
||
if len(updates) > 0 {
|
||
h.db.Model(&area).Updates(updates)
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// DeleteVisitorArea 删除到访区域
|
||
func (h *AdminHandler) DeleteVisitorArea(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
if err := h.db.Delete(&model.VisitorArea{}, id).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ============ 来访目的管理 ============
|
||
|
||
// ListVisitTypes 来访目的列表
|
||
func (h *AdminHandler) ListVisitTypes(c *gin.Context) {
|
||
var types []model.VisitType
|
||
h.db.Order("sort ASC, id ASC").Find(&types)
|
||
if types == nil {
|
||
types = []model.VisitType{}
|
||
}
|
||
response.Success(c, types)
|
||
}
|
||
|
||
// CreateVisitType 创建来访目的
|
||
func (h *AdminHandler) CreateVisitType(c *gin.Context) {
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
Sort int `json:"sort"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
vt := model.VisitType{Name: req.Name, Sort: req.Sort, Status: 1}
|
||
if err := h.db.Create(&vt).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "创建成功", vt)
|
||
}
|
||
|
||
// UpdateVisitType 更新来访目的
|
||
func (h *AdminHandler) UpdateVisitType(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
var vt model.VisitType
|
||
if err := h.db.First(&vt, id).Error; err != nil {
|
||
response.NotFound(c, "类型不存在")
|
||
return
|
||
}
|
||
var req struct {
|
||
Name *string `json:"name"`
|
||
Sort *int `json:"sort"`
|
||
Status *int `json:"status"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
updates := map[string]interface{}{}
|
||
if req.Name != nil {
|
||
updates["name"] = *req.Name
|
||
}
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
if req.Status != nil {
|
||
updates["status"] = *req.Status
|
||
}
|
||
if len(updates) > 0 {
|
||
h.db.Model(&vt).Updates(updates)
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// DeleteVisitType 删除来访目的
|
||
func (h *AdminHandler) DeleteVisitType(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
if err := h.db.Delete(&model.VisitType{}, id).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ============ 预约管理(管理员) ============
|
||
|
||
// ListAppointments 管理员查看所有预约
|
||
func (h *AdminHandler) ListAppointments(c *gin.Context) {
|
||
page, pageSize := utils.ParsePagination(c)
|
||
statusStr := c.DefaultQuery("status", "")
|
||
keyword := c.DefaultQuery("keyword", "")
|
||
|
||
var appointments []model.Appointment
|
||
var total int64
|
||
|
||
query := h.db.Model(&model.Appointment{})
|
||
if statusStr != "" {
|
||
status, _ := strconv.Atoi(statusStr)
|
||
query = query.Where("status = ?", status)
|
||
}
|
||
if keyword != "" {
|
||
query = query.Where("visitor_info LIKE ? OR employee_info LIKE ?",
|
||
"%"+keyword+"%", "%"+keyword+"%")
|
||
}
|
||
|
||
query.Count(&total)
|
||
query.Offset(utils.GetOffset(page, pageSize)).Limit(pageSize).
|
||
Order("created_at DESC").Find(&appointments)
|
||
|
||
response.SuccessPage(c, appointments, total, page, pageSize)
|
||
}
|
||
|
||
// ApproveAppointment 管理员审核预约
|
||
// ============ 新审批流配置 ============
|
||
|
||
// SetDepartmentVPs 设置部门的分管领导用户ID列表
|
||
func (h *AdminHandler) SetDepartmentVPs(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
VpUserIDs []uint `json:"vp_user_ids"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
// 序列化为 JSON 数组字符串
|
||
b, _ := json.Marshal(req.VpUserIDs)
|
||
vpIDsStr := string(b)
|
||
if req.VpUserIDs == nil {
|
||
vpIDsStr = ""
|
||
}
|
||
|
||
if err := h.db.Model(&model.Department{}).Where("id = ?", id).Update("vp_user_ids", vpIDsStr).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// SetDepartmentApprover 设置部门指定审批人列表
|
||
func (h *AdminHandler) SetDepartmentApprover(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
ApproverUserIDs []uint `json:"approver_user_ids"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
// 序列化为 JSON 数组字符串
|
||
b, _ := json.Marshal(req.ApproverUserIDs)
|
||
idsStr := string(b)
|
||
if req.ApproverUserIDs == nil {
|
||
idsStr = ""
|
||
}
|
||
|
||
if err := h.db.Model(&model.Department{}).Where("id = ?", id).Update("approver_user_ids", idsStr).Error; err != nil {
|
||
response.InternalError(c, "更新失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "更新成功", nil)
|
||
}
|
||
|
||
// ============ 待确认员工管理 ============
|
||
|
||
// ImportPendingEmployees 批量导入待确认员工
|
||
func (h *AdminHandler) ImportPendingEmployees(c *gin.Context) {
|
||
var req struct {
|
||
Employees []struct {
|
||
RealName string `json:"real_name" binding:"required"`
|
||
Phone string `json:"phone" binding:"required"`
|
||
DepartmentID uint `json:"department_id" binding:"required"`
|
||
DepartmentName string `json:"department_name"`
|
||
Position string `json:"position"`
|
||
EmployeeType string `json:"employee_type"`
|
||
DingTalkUserID string `json:"dingtalk_user_id"`
|
||
} `json:"employees" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
imported := 0
|
||
updated := 0
|
||
for _, emp := range req.Employees {
|
||
if emp.EmployeeType == "" {
|
||
emp.EmployeeType = "employee"
|
||
}
|
||
|
||
var existing model.PendingEmployee
|
||
result := h.db.Where("phone = ?", emp.Phone).First(&existing)
|
||
if result.RowsAffected > 0 {
|
||
// 已存在 → 更新(岗位调动等情况)
|
||
if err := h.db.Model(&existing).Updates(map[string]interface{}{
|
||
"real_name": emp.RealName,
|
||
"department_id": emp.DepartmentID,
|
||
"department_name": emp.DepartmentName,
|
||
"position": emp.Position,
|
||
"employee_type": emp.EmployeeType,
|
||
"ding_talk_user_id": emp.DingTalkUserID,
|
||
}).Error; err != nil {
|
||
fmt.Printf("更新待确认员工失败(JSON): phone=%s err=%v\n", emp.Phone, err)
|
||
continue
|
||
}
|
||
updated++
|
||
continue
|
||
}
|
||
|
||
pe := model.PendingEmployee{
|
||
RealName: emp.RealName,
|
||
Phone: emp.Phone,
|
||
DepartmentID: emp.DepartmentID,
|
||
DepartmentName: emp.DepartmentName,
|
||
Position: emp.Position,
|
||
EmployeeType: emp.EmployeeType,
|
||
DingTalkUserID: emp.DingTalkUserID,
|
||
}
|
||
if err := h.db.Create(&pe).Error; err != nil {
|
||
fmt.Printf("创建待确认员工失败: phone=%s err=%v\n", emp.Phone, err)
|
||
continue
|
||
}
|
||
imported++
|
||
}
|
||
response.Success(c, gin.H{
|
||
"imported": imported,
|
||
"updated": updated,
|
||
"total": len(req.Employees),
|
||
})
|
||
}
|
||
|
||
// ListPendingEmployees 待确认员工列表
|
||
func (h *AdminHandler) ListPendingEmployees(c *gin.Context) {
|
||
page, pageSize := utils.ParsePagination(c)
|
||
status := c.DefaultQuery("status", "")
|
||
keyword := c.DefaultQuery("keyword", "")
|
||
var list []model.PendingEmployee
|
||
var total int64
|
||
query := h.db.Model(&model.PendingEmployee{})
|
||
if status == "matched" {
|
||
query = query.Where("is_matched = ?", true)
|
||
} else if status == "pending" {
|
||
query = query.Where("is_matched = ?", false)
|
||
}
|
||
if keyword != "" {
|
||
query = query.Where("real_name LIKE ? OR phone LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||
}
|
||
deptIDStr := c.DefaultQuery("department_id", "")
|
||
if deptIDStr != "" {
|
||
deptID, _ := strconv.ParseUint(deptIDStr, 10, 64)
|
||
if deptID > 0 {
|
||
query = query.Where("department_id = ?", deptID)
|
||
}
|
||
}
|
||
query.Count(&total)
|
||
query.Order("created_at DESC").Offset(utils.GetOffset(page, pageSize)).Limit(pageSize).Find(&list)
|
||
if list == nil {
|
||
list = []model.PendingEmployee{}
|
||
}
|
||
response.SuccessPage(c, list, total, page, pageSize)
|
||
}
|
||
|
||
// GetPendingEmployeeStats 待确认员工统计
|
||
func (h *AdminHandler) GetPendingEmployeeStats(c *gin.Context) {
|
||
var total, pending, matched int64
|
||
h.db.Model(&model.PendingEmployee{}).Count(&total)
|
||
h.db.Model(&model.PendingEmployee{}).Where("is_matched = ?", false).Count(&pending)
|
||
h.db.Model(&model.PendingEmployee{}).Where("is_matched = ?", true).Count(&matched)
|
||
response.Success(c, gin.H{
|
||
"total": total,
|
||
"pending": pending,
|
||
"matched": matched,
|
||
})
|
||
}
|
||
|
||
// MatchPendingEmployee 手动匹配待确认员工到用户(按手机号)
|
||
func (h *AdminHandler) MatchPendingEmployee(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
var pending model.PendingEmployee
|
||
if err := h.db.First(&pending, id).Error; err != nil {
|
||
response.NotFound(c, "记录不存在")
|
||
return
|
||
}
|
||
if pending.IsMatched {
|
||
// 已匹配过的也允许重新同步,不阻塞
|
||
}
|
||
|
||
// 按手机号查找用户(不要求 phone_verified,手机号本身即可)
|
||
var user model.User
|
||
if err := h.db.Where("phone = ?", pending.Phone).First(&user).Error; err != nil {
|
||
response.BadRequest(c, "未找到已绑定该手机号的用户,请先让员工登录并绑定手机")
|
||
return
|
||
}
|
||
|
||
now := time.Now()
|
||
// 创建 UserDepartment
|
||
ud := model.UserDepartment{
|
||
UserID: user.ID,
|
||
DepartmentID: pending.DepartmentID,
|
||
DepartmentName: pending.DepartmentName,
|
||
Position: pending.Position,
|
||
EmployeeType: pending.EmployeeType,
|
||
EmployeeStatus: 1,
|
||
DingTalkUserID: pending.DingTalkUserID,
|
||
}
|
||
h.db.Where("user_id = ?", user.ID).Assign(ud).FirstOrCreate(&model.UserDepartment{UserID: user.ID})
|
||
|
||
// 升级角色 + 同步真实姓名
|
||
updates := map[string]interface{}{
|
||
"role": "employee",
|
||
}
|
||
if user.RealName == "" || user.RealName != pending.RealName {
|
||
updates["real_name"] = pending.RealName
|
||
}
|
||
h.db.Model(&user).Updates(updates)
|
||
|
||
// 标记已匹配
|
||
h.db.Model(&pending).Updates(map[string]interface{}{
|
||
"is_matched": true,
|
||
"matched_user_id": &user.ID,
|
||
"matched_at": &now,
|
||
})
|
||
|
||
response.SuccessWithMessage(c, "匹配成功", gin.H{
|
||
"user_id": user.ID,
|
||
"real_name": user.RealName,
|
||
"phone": user.Phone,
|
||
})
|
||
}
|
||
|
||
// SyncAllPendingEmployees 一键同步所有待确认员工信息到用户
|
||
func (h *AdminHandler) SyncAllPendingEmployees(c *gin.Context) {
|
||
var pendings []model.PendingEmployee
|
||
h.db.Find(&pendings)
|
||
|
||
synced := 0
|
||
skipped := 0
|
||
for _, p := range pendings {
|
||
var user model.User
|
||
if err := h.db.Where("phone = ?", p.Phone).First(&user).Error; err != nil {
|
||
skipped++
|
||
continue
|
||
}
|
||
|
||
now := time.Now()
|
||
// 创建或更新 UserDepartment
|
||
ud := model.UserDepartment{
|
||
UserID: user.ID,
|
||
DepartmentID: p.DepartmentID,
|
||
DepartmentName: p.DepartmentName,
|
||
Position: p.Position,
|
||
EmployeeType: p.EmployeeType,
|
||
EmployeeStatus: 1,
|
||
DingTalkUserID: p.DingTalkUserID,
|
||
}
|
||
h.db.Where("user_id = ?", user.ID).Assign(ud).FirstOrCreate(&model.UserDepartment{UserID: user.ID})
|
||
|
||
// 升级角色 + 同步姓名
|
||
userUpdates := map[string]interface{}{
|
||
"role": "employee",
|
||
}
|
||
if user.RealName == "" || user.RealName != p.RealName {
|
||
userUpdates["real_name"] = p.RealName
|
||
}
|
||
h.db.Model(&user).Updates(userUpdates)
|
||
|
||
// 标记已匹配
|
||
h.db.Model(&p).Updates(map[string]interface{}{
|
||
"is_matched": true,
|
||
"matched_user_id": &user.ID,
|
||
"matched_at": &now,
|
||
})
|
||
synced++
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"synced": synced,
|
||
"skipped": skipped,
|
||
"total": len(pendings),
|
||
})
|
||
}
|
||
|
||
// ============ 全局兜底审批人 ============
|
||
|
||
func (h *AdminHandler) ListGlobalApprovers(c *gin.Context) {
|
||
var list []model.GlobalApprover
|
||
h.db.Preload("User").Order("created_at ASC").Find(&list)
|
||
if list == nil {
|
||
list = []model.GlobalApprover{}
|
||
}
|
||
response.Success(c, list)
|
||
}
|
||
|
||
func (h *AdminHandler) CreateGlobalApprover(c *gin.Context) {
|
||
var req struct {
|
||
UserID uint `json:"user_id" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
ga := model.GlobalApprover{UserID: req.UserID}
|
||
if err := h.db.Create(&ga).Error; err != nil {
|
||
response.InternalError(c, "创建失败")
|
||
return
|
||
}
|
||
h.db.Preload("User").First(&ga, ga.ID)
|
||
response.SuccessWithMessage(c, "创建成功", ga)
|
||
}
|
||
|
||
func (h *AdminHandler) DeleteGlobalApprover(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "参数错误")
|
||
return
|
||
}
|
||
if err := h.db.Delete(&model.GlobalApprover{}, id).Error; err != nil {
|
||
response.InternalError(c, "删除失败")
|
||
return
|
||
}
|
||
response.SuccessWithMessage(c, "删除成功", nil)
|
||
}
|
||
|
||
// ImportPendingEmployeesExcel 通过 Excel 文件导入待确认员工
|
||
func (h *AdminHandler) ImportPendingEmployeesExcel(c *gin.Context) {
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
response.BadRequest(c, "请上传 Excel 文件")
|
||
return
|
||
}
|
||
|
||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||
if ext != ".xlsx" && ext != ".xls" {
|
||
response.BadRequest(c, "仅支持 .xlsx 或 .xls 文件")
|
||
return
|
||
}
|
||
|
||
src, err := file.Open()
|
||
if err != nil {
|
||
response.InternalError(c, "无法读取文件")
|
||
return
|
||
}
|
||
defer src.Close()
|
||
|
||
f, err := excelize.OpenReader(src)
|
||
if err != nil {
|
||
response.BadRequest(c, "无法解析 Excel 文件")
|
||
return
|
||
}
|
||
|
||
rows, err := f.GetRows(f.GetSheetName(0))
|
||
if err != nil || len(rows) < 4 {
|
||
response.BadRequest(c, "Excel 文件为空或格式不正确")
|
||
return
|
||
}
|
||
|
||
var departments []model.Department
|
||
h.db.Where("status = 1").Find(&departments)
|
||
deptMap := make(map[string]uint)
|
||
for _, d := range departments {
|
||
deptMap[d.Name] = d.ID
|
||
}
|
||
|
||
var imported, updated, noDept int
|
||
for i := 3; i < len(rows); i++ {
|
||
row := rows[i]
|
||
if len(row) < 4 {
|
||
continue
|
||
}
|
||
dingTalkUserID := strings.TrimSpace(row[0])
|
||
name := strings.TrimSpace(row[1])
|
||
phone := strings.TrimSpace(row[2])
|
||
deptFull := strings.TrimSpace(row[3])
|
||
|
||
if name == "" || phone == "" || deptFull == "" {
|
||
continue
|
||
}
|
||
if !strings.Contains(deptFull, "四川") {
|
||
continue
|
||
}
|
||
|
||
phone = strings.ReplaceAll(phone, "+86-", "")
|
||
phone = strings.ReplaceAll(phone, "+86", "")
|
||
phone = strings.TrimSpace(phone)
|
||
|
||
subDept := deptFull
|
||
if idx := strings.Index(deptFull, "-"); idx >= 0 {
|
||
subDept = strings.TrimSpace(deptFull[idx+1:])
|
||
}
|
||
|
||
deptID, ok := deptMap[subDept]
|
||
if !ok {
|
||
deptID, ok = deptMap[deptFull]
|
||
}
|
||
if !ok {
|
||
noDept++
|
||
continue
|
||
}
|
||
|
||
updates := map[string]interface{}{
|
||
"real_name": name,
|
||
"department_id": deptID,
|
||
"department_name": subDept,
|
||
"position": "",
|
||
"employee_type": "employee",
|
||
"ding_talk_user_id": dingTalkUserID,
|
||
}
|
||
|
||
var existing model.PendingEmployee
|
||
result := h.db.Where("phone = ?", phone).First(&existing)
|
||
if result.RowsAffected > 0 {
|
||
// 已存在 → 更新
|
||
if err := h.db.Model(&existing).Updates(updates).Error; err != nil {
|
||
continue
|
||
}
|
||
updated++
|
||
updated++
|
||
continue
|
||
}
|
||
|
||
pe := model.PendingEmployee{
|
||
RealName: name,
|
||
Phone: phone,
|
||
DepartmentID: deptID,
|
||
DepartmentName: subDept,
|
||
Position: "",
|
||
EmployeeType: "employee",
|
||
DingTalkUserID: dingTalkUserID,
|
||
}
|
||
if err := h.db.Create(&pe).Error; err != nil {
|
||
continue
|
||
}
|
||
imported++
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"imported": imported,
|
||
"updated": updated,
|
||
"noDept": noDept,
|
||
"total": imported + updated + noDept,
|
||
})
|
||
}
|