package logic import ( "context" "strings" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "tool-api/api/admin/v1" "tool-api/internal/consts" "tool-api/internal/model/entity" ) // ============================================================================ // 年会域:企业 / 员工 / 候场名单 // 依据:PRD-02 §4.2、§6.2;架构 §3.2.2-3.2.5、§5.2 A1-A5。 // // 信息架构(PRD-02 §4.2.1): // Enterprise 1 ── n Employee(企业级员工库,跨活动复用) // Event n ── n Employee(关联表 event_participants = 候场名单) // ============================================================================ // ===== 企业 ===== // AdminEnterpriseList 企业列表(含员工数) func AdminEnterpriseList(ctx context.Context, req *v1.EnterpriseListReq) (*v1.EnterpriseListRes, error) { model := g.Model(consts.TableEnterprises) if kw := strings.TrimSpace(req.Keyword); kw != "" { model = model.WhereLike("name", "%"+kw+"%") } total, err := model.Count() if err != nil { return nil, err } page, size := normalizeAdminPage(req.Page, req.PageSize) records, err := model.OrderDesc("id").Page(page, size).All() if err != nil { return nil, err } list := make([]v1.EnterpriseItem, 0, len(records)) for _, r := range records { ent := &entity.Enterprises{} if err = r.Struct(ent); err != nil { continue } empCount, err := g.Model(consts.TableEmployees).Where("enterprise_id", ent.Id).WhereNull("deleted_at").Count() if err != nil { return nil, err } list = append(list, v1.EnterpriseItem{ Id: ent.Id, Name: ent.Name, Contact: ent.Contact, Remark: ent.Remark, Status: ent.Status, EmpCount: int64(empCount), }) } return &v1.EnterpriseListRes{List: list, Total: total}, nil } // AdminEnterpriseSave 建/改企业;status 缺省视为启用。 func AdminEnterpriseSave(ctx context.Context, req *v1.EnterpriseSaveReq) (*v1.EnterpriseSaveRes, error) { now := gtime.Now() status := req.Status if status != 0 && status != 1 { status = 1 } if req.Id > 0 { result, err := g.Model(consts.TableEnterprises).Where("id", req.Id).Data(g.Map{ "name": req.Name, "contact": req.Contact, "remark": req.Remark, "status": status, "updated_at": now, }).Update() if err != nil { return nil, err } if affected, _ := result.RowsAffected(); affected == 0 { // 无字段变化时 RowsAffected 也可能为 0,回查确认存在性 exists, err := g.Model(consts.TableEnterprises).Where("id", req.Id).Count() if err != nil { return nil, err } if exists == 0 { return nil, gerror.New("企业不存在") } } return &v1.EnterpriseSaveRes{Id: req.Id}, nil } id, err := g.Model(consts.TableEnterprises).Data(g.Map{ "name": req.Name, "contact": req.Contact, "remark": req.Remark, "status": status, "created_at": now, "updated_at": now, }).InsertAndGetId() if err != nil { return nil, err } return &v1.EnterpriseSaveRes{Id: id}, nil } // ===== 员工 ===== // AdminEmployeeList 员工库列表;EventId>0 时标记 InEvent(是否已在候场名单)。 func AdminEmployeeList(ctx context.Context, req *v1.EmployeeListReq) (*v1.EmployeeListRes, error) { // 软删除:员工列表默认排除已删员工 model := g.Model(consts.TableEmployees).WhereNull("deleted_at") if req.EnterpriseId > 0 { model = model.Where("enterprise_id", req.EnterpriseId) } if kw := strings.TrimSpace(req.Keyword); kw != "" { like := "%" + kw + "%" model = model.Where("(name LIKE ? OR phone LIKE ?)", like, like) } total, err := model.Count() if err != nil { return nil, err } page, size := normalizeAdminPage(req.Page, req.PageSize) records, err := model.OrderDesc("id").Page(page, size).All() if err != nil { return nil, err } inEvent := map[int64]bool{} excludeAssign := map[int64]int{} if req.EventId > 0 { parts, err := g.Model(consts.TableEventParticipants).Where("event_id", req.EventId).All() if err != nil { return nil, err } for _, p := range parts { empId := p["employee_id"].Int64() inEvent[empId] = true excludeAssign[empId] = p["exclude_assign"].Int() } } list := make([]v1.EmployeeItem, 0, len(records)) for _, r := range records { emp := &entity.Employees{} if err = r.Struct(emp); err != nil { continue } list = append(list, v1.EmployeeItem{ Id: emp.Id, Name: emp.Name, Phone: emp.Phone, Dept: emp.Dept, Remark: emp.Remark, Status: emp.Status, InEvent: inEvent[emp.Id], ExcludeAssign: excludeAssign[emp.Id], }) } return &v1.EmployeeListRes{List: list, Total: total}, nil } // AdminParticipantExclude 批量设置员工「不参与排座」(N4)。仅更新已在候场名单(in_event)的员工。 func AdminParticipantExclude(ctx context.Context, req *v1.ParticipantExcludeReq) (*v1.ParticipantExcludeRes, error) { if _, err := eventRow(ctx, req.EventId); err != nil { return nil, err } now := gtime.Now() model := g.Model(consts.TableEventParticipants). Where("event_id", req.EventId). WhereIn("employee_id", req.EmployeeIds) if _, err := g.Model(consts.TableEventParticipants). Where("event_id", req.EventId). WhereIn("employee_id", req.EmployeeIds). Data(g.Map{"exclude_assign": req.ExcludeAssign, "updated_at": now}). Update(); err != nil { return nil, err } // 值未变化时 UPDATE 的 RowsAffected 可能为 0(默认非 CLIENT_FOUND_ROWS),故以 COUNT 作为「实际生效」口径 updated, err := model.Count() if err != nil { return nil, err } // T17 审计:批量改「不参与排座」属座次配置变更 WriteAudit(ctx, AuditEntry{ Action: "event.participant_exclude", TargetType: "event", TargetId: auditId(req.EventId), After: g.Map{"employee_ids": req.EmployeeIds, "exclude_assign": req.ExcludeAssign, "updated": updated}, Result: consts.AuditResultSuccess, }) return &v1.ParticipantExcludeRes{Success: int(updated)}, nil } // AdminEmployeeSave 新增/修改员工(手工加人);EventId>0 时同时纳入候场名单。 // // 去重键 (enterprise_id, phone):命中已存在记录时返回明确提示,避免直接抛 500。 func AdminEmployeeSave(ctx context.Context, req *v1.EmployeeSaveReq) (*v1.EmployeeSaveRes, error) { if exists, err := g.Model(consts.TableEnterprises).Where("id", req.EnterpriseId).Count(); err != nil { return nil, err } else if exists == 0 { return nil, gerror.New("企业不存在") } phone := normalizePhone(req.Phone) now := gtime.Now() var employeeId int64 if req.Id > 0 { result, err := g.Model(consts.TableEmployees). Where("id", req.Id). Where("enterprise_id", req.EnterpriseId). WhereNull("deleted_at"). Data(g.Map{ "name": req.Name, "phone": phone, "dept": req.Dept, "remark": req.Remark, "updated_at": now, }).Update() if err != nil { return nil, friendlyDbErr(err) } if affected, _ := result.RowsAffected(); affected == 0 { if exists, err := g.Model(consts.TableEmployees).Where("id", req.Id).Where("enterprise_id", req.EnterpriseId).WhereNull("deleted_at").Count(); err != nil { return nil, err } else if exists == 0 { return nil, gerror.New("员工不存在") } } employeeId = req.Id } else { id, err := g.Model(consts.TableEmployees).Data(g.Map{ "enterprise_id": req.EnterpriseId, "name": req.Name, "phone": phone, "dept": req.Dept, "remark": req.Remark, "status": 1, "created_at": now, "updated_at": now, }).InsertAndGetId() if err != nil { return nil, friendlyDbErr(err) } employeeId = id } if req.EventId > 0 { if err := addParticipant(ctx, req.EventId, employeeId); err != nil { return nil, err } } return &v1.EmployeeSaveRes{Id: employeeId}, nil } // AdminEmployeeStatus 员工启停 func AdminEmployeeStatus(ctx context.Context, req *v1.EmployeeStatusReq) (*v1.EmployeeStatusRes, error) { result, err := g.Model(consts.TableEmployees).Where("id", req.Id).WhereNull("deleted_at").Data(g.Map{ "status": req.Status, "updated_at": gtime.Now(), }).Update() if err != nil { return nil, err } if affected, _ := result.RowsAffected(); affected == 0 { if exists, err := g.Model(consts.TableEmployees).Where("id", req.Id).WhereNull("deleted_at").Count(); err != nil { return nil, err } else if exists == 0 { return nil, gerror.New("员工不存在") } } return &v1.EmployeeStatusRes{}, nil } // ===== 候场名单 ===== // addParticipant 幂等把员工纳入活动候场名单(依赖 UNIQUE uk_event_emp)。 func addParticipant(ctx context.Context, eventId, employeeId int64) error { exists, err := g.Model(consts.TableEventParticipants). Where("event_id", eventId).Where("employee_id", employeeId).Count() if err != nil { return err } if exists > 0 { return nil } if _, err = g.Model(consts.TableEventParticipants).Data(g.Map{ "event_id": eventId, "employee_id": employeeId, "created_at": gtime.Now(), }).Insert(); err != nil { // 并发下可能已被插入,忽略唯一键冲突 return nil } return nil } // ===== 辅助 ===== // normalizePhone 入库前统一去空格(PRD-02 §4.2.3「手机号去空格」)。 func normalizePhone(phone string) string { return strings.ReplaceAll(strings.TrimSpace(phone), " ", "") } // friendlyDbErr 把唯一键冲突等 DB 错误转为可读提示,避免 500。 func friendlyDbErr(err error) error { msg := err.Error() if strings.Contains(msg, "Duplicate") || strings.Contains(msg, "uk_ent_phone") || strings.Contains(msg, "1062") { return gerror.New("该手机号在本企业下已存在") } return err } // normalizeAdminPage 管理端分页兜底:page≥1,1≤pageSize≤200。 func normalizeAdminPage(page, size int) (int, int) { if page < 1 { page = 1 } if size <= 0 { size = 20 } if size > 200 { size = 200 } return page, size }