Initial commit

This commit is contained in:
Developer
2026-03-17 12:09:43 +08:00
commit 70bedcf241
211 changed files with 31464 additions and 0 deletions

View File

@@ -0,0 +1,431 @@
# 积分服务开发文档
## 一、Entity 实体类
### UserPoints.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("user_points")
public class UserPoints {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Integer availablePoints;
private Integer frozenPoints;
private Integer totalEarned;
private Integer totalConsumed;
private LocalDate lastSignInDate;
private Integer signInStreak;
private LocalDateTime updatedAt;
}
```
### PointsRecord.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("points_records")
public class PointsRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String pointsType; // earn / consume / freeze / unfreeze
private String source; // register/sign_in/invite/...
private Integer amount;
private Integer balance;
private String description;
private Long relatedId;
private String relatedType;
private LocalDateTime createdAt;
}
```
### PointsRule.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("points_rules")
public class PointsRule {
@TableId(type = IdType.AUTO)
private Integer id;
private String ruleName;
private String source;
private Integer pointsAmount;
private Integer frequencyLimit;
private String frequencyPeriod; // daily/weekly/monthly/unlimited
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
## 二、VO
### PointsBalanceVO.java
```java
package com.openclaw.vo;
import lombok.Data;
import java.time.LocalDate;
@Data
public class PointsBalanceVO {
private Integer availablePoints;
private Integer frozenPoints;
private Integer totalEarned;
private Integer totalConsumed;
private LocalDate lastSignInDate;
private Integer signInStreak;
private Boolean signedInToday; // 今日是否已签到
}
```
### PointsRecordVO.java
```java
package com.openclaw.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PointsRecordVO {
private Long id;
private String pointsType;
private String source;
private String sourceLabel; // 中文描述
private Integer amount;
private Integer balance;
private String description;
private LocalDateTime createdAt;
}
```
## 三、Service 接口
### PointsService.java
```java
package com.openclaw.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.openclaw.vo.*;
public interface PointsService {
/** 初始化用户积分账户(注册时调用) */
void initUserPoints(Long userId);
/** 获取积分余额 */
PointsBalanceVO getBalance(Long userId);
/** 获取积分流水(分页) */
IPage<PointsRecordVO> getRecords(Long userId, int pageNum, int pageSize);
/** 每日签到 */
int signIn(Long userId);
/** 按规则发放积分(注册/邀请/加群/评价等) */
void earnPoints(Long userId, String source, Long relatedId, String relatedType);
/** 消耗积分购买Skill */
void consumePoints(Long userId, int amount, Long relatedId, String relatedType);
/** 冻结积分(下单时) */
void freezePoints(Long userId, int amount, Long orderId);
/** 解冻积分(取消订单时) */
void unfreezePoints(Long userId, int amount, Long orderId);
/** 检查积分是否充足 */
boolean hasEnoughPoints(Long userId, int required);
}
```
## 四、Service 实现
### PointsServiceImpl.java
```java
package com.openclaw.service.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.entity.*;
import com.openclaw.exception.BusinessException;
import com.openclaw.repository.*;
import com.openclaw.service.PointsService;
import com.openclaw.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Service
@RequiredArgsConstructor
public class PointsServiceImpl implements PointsService {
private final UserPointsRepository userPointsRepo;
private final PointsRecordRepository recordRepo;
private final PointsRuleRepository ruleRepo;
@Override
@Transactional
public void initUserPoints(Long userId) {
UserPoints up = new UserPoints();
up.setUserId(userId);
up.setAvailablePoints(0);
up.setFrozenPoints(0);
up.setTotalEarned(0);
up.setTotalConsumed(0);
up.setSignInStreak(0);
userPointsRepo.insert(up);
}
@Override
public PointsBalanceVO getBalance(Long userId) {
UserPoints up = userPointsRepo.findByUserId(userId);
PointsBalanceVO vo = new PointsBalanceVO();
if (up == null) return vo;
vo.setAvailablePoints(up.getAvailablePoints());
vo.setFrozenPoints(up.getFrozenPoints());
vo.setTotalEarned(up.getTotalEarned());
vo.setTotalConsumed(up.getTotalConsumed());
vo.setLastSignInDate(up.getLastSignInDate());
vo.setSignInStreak(up.getSignInStreak());
vo.setSignedInToday(LocalDate.now().equals(up.getLastSignInDate()));
return vo;
}
@Override
public IPage<PointsRecordVO> getRecords(Long userId, int pageNum, int pageSize) {
Page<PointsRecord> page = new Page<>(pageNum, pageSize);
IPage<PointsRecord> result = recordRepo.selectPage(page,
new LambdaQueryWrapper<PointsRecord>()
.eq(PointsRecord::getUserId, userId)
.orderByDesc(PointsRecord::getCreatedAt));
return result.convert(this::toRecordVO);
}
@Override
@Transactional
public int signIn(Long userId) {
UserPoints up = userPointsRepo.findByUserId(userId);
LocalDate today = LocalDate.now();
// 今日已签到
if (today.equals(up.getLastSignInDate())) {
throw new BusinessException(ErrorCode.ALREADY_SIGNED_IN);
}
// 计算连续签到天数
boolean consecutive = up.getLastSignInDate() != null &&
today.minusDays(1).equals(up.getLastSignInDate());
int streak = consecutive ? up.getSignInStreak() + 1 : 1;
// 签到积分连续签到递增最高20分
int points = Math.min(5 + (streak - 1) * 1, 20);
up.setLastSignInDate(today);
up.setSignInStreak(streak);
userPointsRepo.updateById(up);
addPoints(userId, "earn", "sign_in", points, points, "每日签到", null, null);
return points;
}
@Override
@Transactional
public void earnPoints(Long userId, String source, Long relatedId, String relatedType) {
PointsRule rule = ruleRepo.findBySource(source);
if (rule == null || !rule.getEnabled()) return;
UserPoints up = userPointsRepo.findByUserId(userId);
int newBalance = up.getAvailablePoints() + rule.getPointsAmount();
addPoints(userId, "earn", source, rule.getPointsAmount(), newBalance,
rule.getRuleName(), relatedId, relatedType);
}
@Override
@Transactional
public void consumePoints(Long userId, int amount, Long relatedId, String relatedType) {
UserPoints up = userPointsRepo.findByUserId(userId);
if (up.getAvailablePoints() < amount) throw new BusinessException(ErrorCode.POINTS_NOT_ENOUGH);
int newBalance = up.getAvailablePoints() - amount;
addPoints(userId, "consume", "skill_purchase", -amount, newBalance,
"兑换Skill", relatedId, relatedType);
}
@Override
@Transactional
public void freezePoints(Long userId, int amount, Long orderId) {
userPointsRepo.freezePoints(userId, amount);
addPoints(userId, "freeze", "skill_purchase", -amount,
userPointsRepo.findByUserId(userId).getAvailablePoints(),
"积分冻结-订单" + orderId, orderId, "order");
}
@Override
@Transactional
public void unfreezePoints(Long userId, int amount, Long orderId) {
userPointsRepo.unfreezePoints(userId, amount);
addPoints(userId, "unfreeze", "skill_purchase", amount,
userPointsRepo.findByUserId(userId).getAvailablePoints(),
"积分解冻-订单取消" + orderId, orderId, "order");
}
@Override
public boolean hasEnoughPoints(Long userId, int required) {
UserPoints up = userPointsRepo.findByUserId(userId);
return up != null && up.getAvailablePoints() >= required;
}
private void addPoints(Long userId, String type, String source, int amount,
int balance, String desc, Long relatedId, String relatedType) {
// 更新账户
if ("earn".equals(type)) {
userPointsRepo.addAvailablePoints(userId, amount);
} else if ("consume".equals(type)) {
userPointsRepo.addAvailablePoints(userId, amount); // amount为负数
userPointsRepo.addTotalConsumed(userId, -amount);
}
userPointsRepo.addTotalEarned(userId, "earn".equals(type) ? amount : 0);
// 记录流水
PointsRecord r = new PointsRecord();
r.setUserId(userId);
r.setPointsType(type);
r.setSource(source);
r.setAmount(amount);
r.setBalance(balance);
r.setDescription(desc);
r.setRelatedId(relatedId);
r.setRelatedType(relatedType);
recordRepo.insert(r);
}
private PointsRecordVO toRecordVO(PointsRecord r) {
PointsRecordVO vo = new PointsRecordVO();
vo.setId(r.getId());
vo.setPointsType(r.getPointsType());
vo.setSource(r.getSource());
vo.setSourceLabel(getSourceLabel(r.getSource()));
vo.setAmount(r.getAmount());
vo.setBalance(r.getBalance());
vo.setDescription(r.getDescription());
vo.setCreatedAt(r.getCreatedAt());
return vo;
}
@Override
@Transactional
public void addPointsDirectly(Long userId, int amount, String source,
Long relatedId, String desc) {
UserPoints up = userPointsRepo.findByUserId(userId);
if (up == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
int newBalance = up.getAvailablePoints() + amount;
if (newBalance < 0) throw new BusinessException(ErrorCode.POINTS_NOT_ENOUGH);
String type = amount >= 0 ? "earn" : "consume";
addPoints(userId, type, source, amount, newBalance, desc, relatedId, null);
}
@Override
public void ensureNotNegative(Long userId) {
UserPoints up = userPointsRepo.findByUserId(userId);
if (up != null && up.getAvailablePoints() < 0) {
// 强制归零,记录一条修正流水
int diff = -up.getAvailablePoints();
addPoints(userId, "admin_correct", "admin_adjust", diff, 0,
"积分余额修正(防负)", null, null);
}
}
private String getSourceLabel(String source) {
return switch (source) {
case "register" -> "新用户注册";
case "sign_in" -> "每日签到";
case "invite" -> "邀请好友";
case "join_community" -> "加入社群";
case "recharge" -> "充值赠送";
case "skill_purchase" -> "兑换Skill";
case "review" -> "发表评价";
case "activity" -> "活动奖励";
case "admin_adjust" -> "管理员调整";
default -> source;
};
}
}
```
## 五、Controller
### PointsController.java
```java
package com.openclaw.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.openclaw.common.Result;
import com.openclaw.service.PointsService;
import com.openclaw.util.UserContext;
import com.openclaw.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/points")
@RequiredArgsConstructor
public class PointsController {
private final PointsService pointsService;
/** 获取积分余额 */
@GetMapping("/balance")
public Result<PointsBalanceVO> getBalance() {
return Result.ok(pointsService.getBalance(UserContext.getUserId()));
}
/** 获取积分流水 */
@GetMapping("/records")
public Result<IPage<PointsRecordVO>> getRecords(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
return Result.ok(pointsService.getRecords(UserContext.getUserId(), pageNum, pageSize));
}
/** 每日签到 */
@PostMapping("/sign-in")
public Result<Integer> signIn() {
int earned = pointsService.signIn(UserContext.getUserId());
return Result.ok(earned);
}
}
```
---
**文档版本**v1.0
**创建日期**2026-03-16