246 lines
7.5 KiB
Go
246 lines
7.5 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/gogf/gf/v2/errors/gerror"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
|
||
v1 "tool-api/api/admin/v1"
|
||
"tool-api/internal/consts"
|
||
)
|
||
|
||
// ============================================================================
|
||
// 批量操作(T15 批量改值 + T18 软删除)
|
||
//
|
||
// 设计要点:
|
||
// - 幂等:同一批重复提交结果一致;把值改为当前值仍算成功;对**已软删**的 id 再次
|
||
// 执行 delete 也计成功(不塞进 failures);
|
||
// - 部分成功:不存在的 id 计入 failed 并附原因,不影响其余 id 的更新;
|
||
// - 高效:存在性一次 IN 查询、更新一次 IN 批量,避免 N 次单条请求。
|
||
//
|
||
// action 白名单:user = status / level / delete;tool = is_enabled / is_hot / delete;
|
||
// employee = status / delete。
|
||
// ============================================================================
|
||
|
||
// dedupBatchIds 去重并剔除非法 id(<=0),保持首次出现顺序。
|
||
func dedupBatchIds(in []int64) []int64 {
|
||
seen := make(map[int64]bool, len(in))
|
||
out := make([]int64, 0, len(in))
|
||
for _, id := range in {
|
||
if id <= 0 || seen[id] {
|
||
continue
|
||
}
|
||
seen[id] = true
|
||
out = append(out, id)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// partitionBatchIds 按「是否存在」把 id 分为可操作(ok)与缺失(missing),保持入参顺序。
|
||
func partitionBatchIds(requested []int64, existing map[int64]bool) (ok []int64, missing []int64) {
|
||
ok = make([]int64, 0, len(requested))
|
||
missing = make([]int64, 0)
|
||
for _, id := range requested {
|
||
if existing[id] {
|
||
ok = append(ok, id)
|
||
continue
|
||
}
|
||
missing = append(missing, id)
|
||
}
|
||
return ok, missing
|
||
}
|
||
|
||
// batchFailures 把缺失 id 转为失败明细。
|
||
func batchFailures(missing []int64, reason string) []v1.BatchFailure {
|
||
out := make([]v1.BatchFailure, 0, len(missing))
|
||
for _, id := range missing {
|
||
out = append(out, v1.BatchFailure{Id: id, Reason: reason})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// existingIdSet 一次 IN 查询出「实际存在的 id 集合」。
|
||
//
|
||
// 🔴 必须 `.Unscoped()`:GoFrame v2.10 会给所有 Model 查询(select/count/update/delete)
|
||
// **自动**追加 `deleted_at IS NULL`(见 softdelete.go 头部不变量)。若不加 Unscoped,
|
||
// 已软删的行会被自动过滤掉 → 落进 missing → 被判为「不存在」→ 返回 failed,
|
||
// 直接违反「重复删除已删 id 计成功」的幂等契约(且代码注释与实现相反,极隐蔽)。
|
||
// Unscoped 让软删行照常出现在结果里、计入 ok —— delete 幂等正是依赖此语义。
|
||
func existingIdSet(ctx context.Context, table string, ids []int64) (map[int64]bool, error) {
|
||
set := make(map[int64]bool, len(ids))
|
||
if len(ids) == 0 {
|
||
return set, nil
|
||
}
|
||
records, err := g.Model(table).Unscoped().Fields("id").WhereIn("id", ids).All()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, r := range records {
|
||
set[r["id"].Int64()] = true
|
||
}
|
||
return set, nil
|
||
}
|
||
|
||
// normalizeToggle 归一化为 0/1。
|
||
func normalizeToggle(v int) int {
|
||
if v == 1 {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// AdminUserBatch 批量操作用户:status(启停)/ level(改等级)/ delete(软删除,可恢复)。
|
||
func AdminUserBatch(ctx context.Context, req *v1.UserBatchReq) (*v1.UserBatchRes, error) {
|
||
ids := dedupBatchIds(req.Ids)
|
||
if len(ids) == 0 {
|
||
return &v1.UserBatchRes{Failures: []v1.BatchFailure{}}, nil
|
||
}
|
||
existing, err := existingIdSet(ctx, consts.TableUsers, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ok, missing := partitionBatchIds(ids, existing)
|
||
|
||
switch req.Action {
|
||
case "delete":
|
||
// 软删除:墓碑化 openid/username,保证可重新注册;已删 id 幂等计成功。
|
||
if err = softDeleteUsers(ctx, ok); err != nil {
|
||
return nil, err
|
||
}
|
||
case "status", "level":
|
||
data := g.Map{}
|
||
if req.Action == "status" {
|
||
data["status"] = normalizeToggle(req.Status)
|
||
} else {
|
||
n, err := g.Model(consts.TableLevels).Where("level_key", req.LevelKey).Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if n == 0 {
|
||
return nil, gerror.New("等级不存在") // 参数错误 → 整体失败,不做「部分成功」
|
||
}
|
||
data["level_key"] = req.LevelKey
|
||
}
|
||
if len(ok) > 0 {
|
||
if _, err = g.Model(consts.TableUsers).WhereIn("id", ok).Data(data).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
default:
|
||
return nil, gerror.New("不支持的批量动作")
|
||
}
|
||
|
||
res := &v1.UserBatchRes{
|
||
Success: len(ok),
|
||
Failed: len(missing),
|
||
Failures: batchFailures(missing, "用户不存在"),
|
||
}
|
||
WriteAudit(ctx, AuditEntry{
|
||
Action: "user.batch_" + req.Action,
|
||
TargetType: "user",
|
||
TargetId: "batch",
|
||
After: g.Map{
|
||
"action": req.Action, "ids": ok, "status": req.Status, "level_key": req.LevelKey,
|
||
"success": res.Success, "failed": res.Failed,
|
||
},
|
||
Result: consts.AuditResultSuccess,
|
||
})
|
||
return res, nil
|
||
}
|
||
|
||
// AdminToolsBatch 批量操作工具:is_enabled / is_hot(改值)/ delete(软删除,可恢复)。
|
||
func AdminToolsBatch(ctx context.Context, req *v1.ToolsBatchReq) (*v1.ToolsBatchRes, error) {
|
||
ids := dedupBatchIds(req.Ids)
|
||
if len(ids) == 0 {
|
||
return &v1.ToolsBatchRes{Failures: []v1.BatchFailure{}}, nil
|
||
}
|
||
existing, err := existingIdSet(ctx, consts.TableTools, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ok, missing := partitionBatchIds(ids, existing)
|
||
|
||
switch req.Action {
|
||
case "delete":
|
||
// 软删除:仅置 deleted_at(不墓碑化 tool_key,见 softdelete.go 说明)。
|
||
if err = softDeleteSimple(ctx, consts.TableTools, ok); err != nil {
|
||
return nil, err
|
||
}
|
||
case "is_enabled", "is_hot":
|
||
if len(ok) > 0 {
|
||
if _, err = g.Model(consts.TableTools).WhereIn("id", ok).
|
||
Data(g.Map{req.Action: normalizeToggle(req.Value)}).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
default:
|
||
return nil, gerror.New("不支持的批量动作")
|
||
}
|
||
|
||
res := &v1.ToolsBatchRes{
|
||
Success: len(ok),
|
||
Failed: len(missing),
|
||
Failures: batchFailures(missing, "工具不存在"),
|
||
}
|
||
WriteAudit(ctx, AuditEntry{
|
||
Action: "tool.batch_" + req.Action,
|
||
TargetType: "tool",
|
||
TargetId: "batch",
|
||
After: g.Map{
|
||
"action": req.Action, "value": normalizeToggle(req.Value), "ids": ok,
|
||
"success": res.Success, "failed": res.Failed,
|
||
},
|
||
Result: consts.AuditResultSuccess,
|
||
})
|
||
return res, nil
|
||
}
|
||
|
||
// AdminEmployeeBatch 批量操作员工:status(启停)/ delete(软删除,可恢复)。
|
||
func AdminEmployeeBatch(ctx context.Context, req *v1.EmployeeBatchReq) (*v1.EmployeeBatchRes, error) {
|
||
ids := dedupBatchIds(req.Ids)
|
||
if len(ids) == 0 {
|
||
return &v1.EmployeeBatchRes{Failures: []v1.BatchFailure{}}, nil
|
||
}
|
||
existing, err := existingIdSet(ctx, consts.TableEmployees, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ok, missing := partitionBatchIds(ids, existing)
|
||
|
||
switch req.Action {
|
||
case "delete":
|
||
// 软删除:墓碑化 phone,保证同企业同手机号可重新录入。
|
||
if err = softDeleteEmployees(ctx, ok); err != nil {
|
||
return nil, err
|
||
}
|
||
case "status":
|
||
if len(ok) > 0 {
|
||
if _, err = g.Model(consts.TableEmployees).WhereIn("id", ok).
|
||
Data(g.Map{"status": normalizeToggle(req.Status)}).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
default:
|
||
return nil, gerror.New("不支持的批量动作")
|
||
}
|
||
|
||
res := &v1.EmployeeBatchRes{
|
||
Success: len(ok),
|
||
Failed: len(missing),
|
||
Failures: batchFailures(missing, "员工不存在"),
|
||
}
|
||
// T17/T18:员工批量也纳入审计(与 user/tool 批量保持一致)
|
||
WriteAudit(ctx, AuditEntry{
|
||
Action: "employee.batch_" + req.Action,
|
||
TargetType: "employee",
|
||
TargetId: "batch",
|
||
After: g.Map{
|
||
"action": req.Action, "ids": ok, "status": req.Status,
|
||
"success": res.Success, "failed": res.Failed,
|
||
},
|
||
Result: consts.AuditResultSuccess,
|
||
})
|
||
return res, nil
|
||
}
|