Files
number/后端架构设计/10-管理后台-part2-Service.md
2026-03-17 12:09:43 +08:00

273 lines
11 KiB
Markdown
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.

# 管理后台开发文档 - Part 2AdminService 接口 + 实现)
## 一、AdminService 接口
```java
package com.openclaw.service.admin;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.openclaw.dto.admin.*;
import com.openclaw.entity.PointsRule;
import com.openclaw.vo.admin.*;
import java.util.List;
public interface AdminService {
// 看板
DashboardVO getDashboard();
// 用户
IPage<AdminUserVO> listUsers(AdminUserQueryDTO query);
AdminUserVO getUserDetail(Long userId);
void banUser(Long userId, String reason);
void unbanUser(Long userId);
void adjustPoints(Long userId, int delta, String remark);
// Skill 审核
IPage<AdminSkillVO> listSkills(AdminSkillQueryDTO query);
void auditSkill(SkillAuditDTO dto, Long auditorId);
void offlineSkill(Long skillId, String reason);
// 订单 / 退款
IPage<AdminOrderVO> listOrders(AdminOrderQueryDTO query);
void processRefund(Long refundId, String action, String remark, Long operatorId);
// 积分规则
List<PointsRule> listPointsRules();
void updatePointsRule(Long ruleId, int points);
}
```
## 二、AdminServiceImpl.java
```java
package com.openclaw.service.admin.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.openclaw.constant.ErrorCode;
import com.openclaw.dto.admin.*;
import com.openclaw.entity.*;
import com.openclaw.exception.BusinessException;
import com.openclaw.repository.*;
import com.openclaw.service.PointsService;
import com.openclaw.service.admin.AdminService;
import com.openclaw.vo.admin.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AdminServiceImpl implements AdminService {
private final UserRepository userRepo;
private final SkillRepository skillRepo;
private final OrderRepository orderRepo;
private final OrderRefundRepository refundRepo;
private final PointsRuleRepository pointsRuleRepo;
private final SkillDownloadRepository downloadRepo;
private final PointsService pointsService;
// ---------- 看板 ----------
@Override
public DashboardVO getDashboard() {
DashboardVO vo = new DashboardVO();
LocalDateTime dayStart = LocalDate.now().atStartOfDay();
vo.setTotalUsers(userRepo.selectCount(null));
vo.setTodayNewUsers(userRepo.selectCount(
new LambdaQueryWrapper<User>().ge(User::getCreatedAt, dayStart)));
vo.setActiveUsersLast7d(
userRepo.countActiveUsersAfter(LocalDateTime.now().minusDays(7)));
vo.setTotalOrders(orderRepo.selectCount(null));
vo.setOrdersToday(orderRepo.selectCount(
new LambdaQueryWrapper<Order>().ge(Order::getCreatedAt, dayStart)));
BigDecimal rev = orderRepo.sumCashAmount("paid");
vo.setTotalRevenue(rev == null ? BigDecimal.ZERO : rev);
BigDecimal revToday = orderRepo.sumCashAmountAfter("paid", dayStart);
vo.setRevenueToday(revToday == null ? BigDecimal.ZERO : revToday);
vo.setTotalSkills(skillRepo.selectCount(null));
vo.setPendingAuditSkills(skillRepo.selectCount(
new LambdaQueryWrapper<Skill>().eq(Skill::getStatus, "pending")));
vo.setTotalDownloads(downloadRepo.selectCount(null));
return vo;
}
// ---------- 用户 ----------
@Override
public IPage<AdminUserVO> listUsers(AdminUserQueryDTO q) {
return userRepo.selectPage(new Page<>(q.getPageNum(), q.getPageSize()),
new LambdaQueryWrapper<User>()
.and(q.getKeyword() != null, w -> w
.like(User::getNickname, q.getKeyword()).or()
.like(User::getPhone, q.getKeyword()))
.eq(q.getStatus() != null, User::getStatus, q.getStatus())
.orderByDesc(User::getCreatedAt)
).convert(this::toUserVO);
}
@Override
public AdminUserVO getUserDetail(Long userId) {
User u = userRepo.selectById(userId);
if (u == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
return toUserVO(u);
}
@Override @Transactional
public void banUser(Long userId, String reason) {
User u = requireUser(userId);
u.setStatus("banned"); u.setBanReason(reason);
userRepo.updateById(u);
}
@Override @Transactional
public void unbanUser(Long userId) {
User u = requireUser(userId);
u.setStatus("active"); u.setBanReason(null);
userRepo.updateById(u);
}
@Override @Transactional
public void adjustPoints(Long userId, int delta, String remark) {
String type = delta > 0 ? "admin_add" : "admin_deduct";
String desc = remark != null ? remark : (delta > 0 ? "管理员补积分" : "管理员扣积分");
pointsService.addPointsDirectly(userId, Math.abs(delta), type, null, desc);
}
// ---------- Skill ----------
@Override
public IPage<AdminSkillVO> listSkills(AdminSkillQueryDTO q) {
return skillRepo.selectPage(new Page<>(q.getPageNum(), q.getPageSize()),
new LambdaQueryWrapper<Skill>()
.like(q.getKeyword() != null, Skill::getName, q.getKeyword())
.eq(q.getStatus() != null, Skill::getStatus, q.getStatus())
.eq(q.getCategoryId() != null, Skill::getCategoryId, q.getCategoryId())
.orderByDesc(Skill::getCreatedAt)
).convert(this::toSkillVO);
}
@Override @Transactional
public void auditSkill(SkillAuditDTO dto, Long auditorId) {
Skill s = skillRepo.selectById(dto.getSkillId());
if (s == null) throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
if (!"pending".equals(s.getStatus())) throw new BusinessException(ErrorCode.SKILL_STATUS_ERROR);
switch (dto.getAction()) {
case "approve" -> s.setStatus("approved");
case "reject" -> { s.setStatus("rejected"); s.setRejectReason(dto.getRejectReason()); }
default -> throw new BusinessException(ErrorCode.PARAM_ERROR);
}
s.setAuditorId(auditorId);
s.setAuditedAt(LocalDateTime.now());
skillRepo.updateById(s);
log.info("Skill审核 id={} action={} auditor={}", dto.getSkillId(), dto.getAction(), auditorId);
}
@Override @Transactional
public void offlineSkill(Long skillId, String reason) {
Skill s = skillRepo.selectById(skillId);
if (s == null) throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
s.setStatus("offline"); s.setRejectReason(reason);
skillRepo.updateById(s);
}
// ---------- 订单 ----------
@Override
public IPage<AdminOrderVO> listOrders(AdminOrderQueryDTO q) {
return orderRepo.selectPage(new Page<>(q.getPageNum(), q.getPageSize()),
new LambdaQueryWrapper<Order>()
.like(q.getKeyword() != null, Order::getOrderNo, q.getKeyword())
.eq(q.getStatus() != null, Order::getStatus, q.getStatus())
.ge(q.getStartDate() != null, Order::getCreatedAt, q.getStartDate() != null ? q.getStartDate().atStartOfDay() : null)
.le(q.getEndDate() != null, Order::getCreatedAt, q.getEndDate() != null ? q.getEndDate().plusDays(1).atStartOfDay() : null)
.orderByDesc(Order::getCreatedAt)
).convert(this::toOrderVO);
}
@Override @Transactional
public void processRefund(Long refundId, String action, String remark, Long operatorId) {
OrderRefund rf = refundRepo.selectById(refundId);
if (rf == null) throw new BusinessException(ErrorCode.REFUND_NOT_FOUND);
if (!"pending".equals(rf.getStatus())) throw new BusinessException(ErrorCode.REFUND_STATUS_ERROR);
Order o = orderRepo.selectById(rf.getOrderId());
switch (action) {
case "approve" -> {
rf.setStatus("approved"); o.setStatus("refunded");
if (rf.getRefundPoints() != null && rf.getRefundPoints() > 0)
pointsService.addPointsDirectly(
o.getUserId(), rf.getRefundPoints(), "refund", rf.getId(), "退款返还积分");
// TODO: 调用支付渠道退款
}
case "reject" -> { rf.setStatus("rejected"); o.setStatus("paid"); }
default -> throw new BusinessException(ErrorCode.PARAM_ERROR);
}
rf.setRemark(remark); rf.setOperatorId(operatorId); rf.setProcessedAt(LocalDateTime.now());
refundRepo.updateById(rf); orderRepo.updateById(o);
}
// ---------- 积分规则 ----------
@Override
public List<PointsRule> listPointsRules() { return pointsRuleRepo.selectList(null); }
@Override @Transactional
public void updatePointsRule(Long ruleId, int points) {
PointsRule r = pointsRuleRepo.selectById(ruleId);
if (r == null) throw new BusinessException(ErrorCode.POINTS_RULE_NOT_FOUND);
r.setPoints(points); pointsRuleRepo.updateById(r);
}
// ---------- 私有辅助 ----------
private User requireUser(Long id) {
User u = userRepo.selectById(id);
if (u == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
return u;
}
private AdminUserVO toUserVO(User u) {
AdminUserVO vo = new AdminUserVO();
vo.setId(u.getId()); vo.setPhone(u.getPhone());
vo.setNickname(u.getNickname()); vo.setAvatarUrl(u.getAvatarUrl());
vo.setStatus(u.getStatus()); vo.setCreatedAt(u.getCreatedAt());
return vo;
}
private AdminSkillVO toSkillVO(Skill s) {
AdminSkillVO vo = new AdminSkillVO();
vo.setId(s.getId()); vo.setName(s.getName());
vo.setCoverImageUrl(s.getCoverImageUrl()); vo.setPrice(s.getPrice());
vo.setIsFree(s.getIsFree()); vo.setStatus(s.getStatus());
vo.setCreatorId(s.getCreatorId()); vo.setCreatedAt(s.getCreatedAt());
vo.setAuditedAt(s.getAuditedAt()); vo.setRejectReason(s.getRejectReason());
return vo;
}
private AdminOrderVO toOrderVO(Order o) {
AdminOrderVO vo = new AdminOrderVO();
vo.setId(o.getId()); vo.setOrderNo(o.getOrderNo());
vo.setUserId(o.getUserId()); vo.setTotalAmount(o.getTotalAmount());
vo.setCashAmount(o.getCashAmount()); vo.setPointsUsed(o.getPointsUsed());
vo.setStatus(o.getStatus()); vo.setPaymentMethod(o.getPaymentMethod());
vo.setCreatedAt(o.getCreatedAt()); vo.setPaidAt(o.getPaidAt());
return vo;
}
}
```
---
**文档版本**v1.0 | **创建日期**2026-03-16