331 lines
9.9 KiB
Go
331 lines
9.9 KiB
Go
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
|
||
}
|