Files
qitongxue-api/internal/logic/event_enterprise.go
2026-09-29 10:57:01 +08:00

331 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package 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
}