新增抽奖功能

This commit is contained in:
tianfeng 2025-10-18 14:58:41 +08:00
parent 4f9295b41a
commit 0ee3f41c1b
86 changed files with 4926 additions and 0 deletions

View File

@ -0,0 +1,125 @@
# 抽奖系统数据库初始化SQL
## 1. 抽奖活动表
```sql
CREATE TABLE lottery_activity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_code VARCHAR(50) UNIQUE NOT NULL COMMENT '活动编码',
activity_name VARCHAR(100) NOT NULL COMMENT '活动名称',
activity_desc VARCHAR(500) COMMENT '活动描述',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME NOT NULL COMMENT '结束时间',
status TINYINT DEFAULT 0 COMMENT '状态0-未开始1-进行中2-已结束3-已关闭',
total_stock INT DEFAULT 0 COMMENT '总奖品库存',
consumed_stock INT DEFAULT 0 COMMENT '已消耗库存',
draw_count_limit INT DEFAULT 0 COMMENT '每人抽奖次数限制0为不限制',
draw_count_cycle VARCHAR(20) COMMENT '次数限制周期DAILY-每天TOTAL-总计',
need_ticket TINYINT DEFAULT 0 COMMENT '是否需要抽奖券0-否1-是',
ticket_cost INT DEFAULT 1 COMMENT '每次消耗抽奖券数量',
sort_order INT DEFAULT 0 COMMENT '排序',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_time (status, start_time, end_time),
INDEX idx_code (activity_code)
) COMMENT '抽奖活动表';
```
## 2. 奖品配置表
```sql
CREATE TABLE lottery_prize (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL COMMENT '活动ID',
prize_code VARCHAR(50) NOT NULL COMMENT '奖品编码',
prize_name VARCHAR(100) NOT NULL COMMENT '奖品名称',
prize_type TINYINT NOT NULL COMMENT '奖品类型1-实物2-虚拟币3-优惠券4-积分5-谢谢参与',
prize_value DECIMAL(10,2) COMMENT '奖品价值/数量',
prize_image VARCHAR(200) COMMENT '奖品图片',
prize_level TINYINT COMMENT '奖品等级1-特等奖2-一等奖3-二等奖...',
total_stock INT NOT NULL COMMENT '总库存',
remaining_stock INT NOT NULL COMMENT '剩余库存',
daily_stock INT DEFAULT 0 COMMENT '每日库存限制0为不限制',
probability DECIMAL(10,6) NOT NULL COMMENT '中奖概率0-1之间',
sort_order INT DEFAULT 0 COMMENT '排序(九宫格位置)',
is_display TINYINT DEFAULT 1 COMMENT '是否展示0-否1-是',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_activity (activity_id),
INDEX idx_code (prize_code),
UNIQUE KEY uk_activity_code (activity_id, prize_code)
) COMMENT '奖品配置表';
```
## 3. 中奖记录表
```sql
CREATE TABLE lottery_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
record_no VARCHAR(50) UNIQUE NOT NULL COMMENT '记录编号',
user_id BIGINT NOT NULL COMMENT '用户ID',
activity_id BIGINT NOT NULL COMMENT '活动ID',
prize_id BIGINT COMMENT '奖品ID未中奖为NULL',
prize_name VARCHAR(100) COMMENT '奖品名称快照',
prize_type TINYINT COMMENT '奖品类型快照',
is_win TINYINT NOT NULL COMMENT '是否中奖0-未中奖1-中奖',
draw_time DATETIME NOT NULL COMMENT '抽奖时间',
prize_status TINYINT DEFAULT 0 COMMENT '奖品状态0-待发放1-已发放2-发放失败',
deliver_time DATETIME COMMENT '发放时间',
remark VARCHAR(500) COMMENT '备注',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_activity (user_id, activity_id),
INDEX idx_draw_time (draw_time),
INDEX idx_activity_win (activity_id, is_win),
INDEX idx_record_no (record_no)
) COMMENT '中奖记录表';
```
## 4. 抽奖券表
```sql
CREATE TABLE lottery_ticket (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
ticket_source VARCHAR(50) COMMENT '来源SYSTEM-系统发放ACTIVITY-活动赠送PURCHASE-购买',
source_id VARCHAR(50) COMMENT '来源ID',
total_count INT NOT NULL COMMENT '总数量',
used_count INT DEFAULT 0 COMMENT '已使用数量',
remaining_count INT NOT NULL COMMENT '剩余数量',
expire_time DATETIME COMMENT '过期时间',
status TINYINT DEFAULT 1 COMMENT '状态0-已失效1-正常',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_status (user_id, status),
INDEX idx_expire (expire_time)
) COMMENT '抽奖券表';
```
## 5. 抽奖券使用记录表
```sql
CREATE TABLE lottery_ticket_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
ticket_id BIGINT NOT NULL COMMENT '抽奖券ID',
change_count INT NOT NULL COMMENT '变动数量(正数增加,负数减少)',
change_type TINYINT NOT NULL COMMENT '变动类型1-发放2-消耗3-过期4-退回',
lottery_record_id BIGINT COMMENT '关联的抽奖记录ID',
remark VARCHAR(200) COMMENT '备注',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_ticket (ticket_id),
INDEX idx_lottery_record (lottery_record_id)
) COMMENT '抽奖券使用记录表';
```
## 6. 用户抽奖次数统计表
```sql
CREATE TABLE lottery_user_count (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
activity_id BIGINT NOT NULL COMMENT '活动ID',
draw_date DATE NOT NULL COMMENT '统计日期',
draw_count INT DEFAULT 0 COMMENT '抽奖次数',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_activity_date (user_id, activity_id, draw_date),
INDEX idx_user_activity (user_id, activity_id)
) COMMENT '用户抽奖次数统计表';
```

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 抽奖活动Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryActivityClient", url = "${feign.other.url}" +
LotteryActivityClientApi.API_PREFIX)
public interface LotteryActivityClient extends LotteryActivityClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityManageClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 抽奖活动管理Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryActivityManageClient", url = "${feign.other.url}" +
LotteryActivityManageClientApi.API_PREFIX)
public interface LotteryActivityManageClient extends LotteryActivityManageClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityQueryClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 活动查询Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryActivityQueryClient", url = "${feign.other.url}" +
LotteryActivityQueryClientApi.API_PREFIX)
public interface LotteryActivityQueryClient extends LotteryActivityQueryClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryPrizeClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 奖品配置Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryPrizeClient", url = "${feign.other.url}" +
LotteryPrizeClientApi.API_PREFIX)
public interface LotteryPrizeClient extends LotteryPrizeClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryPrizeManageClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 奖品管理Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryPrizeManageClient", url = "${feign.other.url}" +
LotteryPrizeManageClientApi.API_PREFIX)
public interface LotteryPrizeManageClient extends LotteryPrizeManageClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryRecordClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 中奖记录Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryRecordClient", url = "${feign.other.url}" +
LotteryRecordClientApi.API_PREFIX)
public interface LotteryRecordClient extends LotteryRecordClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryRecordManageClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 中奖记录管理Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryRecordManageClient", url = "${feign.other.url}" +
LotteryRecordManageClientApi.API_PREFIX)
public interface LotteryRecordManageClient extends LotteryRecordManageClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryTicketClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 抽奖券Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryTicketClient", url = "${feign.other.url}" +
LotteryTicketClientApi.API_PREFIX)
public interface LotteryTicketClient extends LotteryTicketClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryTicketManageClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 抽奖券管理Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryTicketManageClient", url = "${feign.other.url}" +
LotteryTicketManageClientApi.API_PREFIX)
public interface LotteryTicketManageClient extends LotteryTicketManageClientApi {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.inner.endpoint.activity;
import com.red.circle.other.inner.endpoint.activity.api.LotteryUserCountClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 用户抽奖次数统计Client.
*
* @author system
* @since 2025-10-18
*/
@FeignClient(name = "lotteryUserCountClient", url = "${feign.other.url}" +
LotteryUserCountClientApi.API_PREFIX)
public interface LotteryUserCountClient extends LotteryUserCountClientApi {
}

View File

@ -0,0 +1,40 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 抽奖活动Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityClientApi {
String API_PREFIX = "/lottery-activity/client";
/**
* 根据活动编码获取活动信息.
*/
@GetMapping("/getByCode")
ResultResponse<LotteryActivityDTO> getByActivityCode(@RequestParam("activityCode") String activityCode);
/**
* 根据活动ID获取活动信息.
*/
@GetMapping("/getById")
ResultResponse<LotteryActivityDTO> getById(@RequestParam("id") Long id);
/**
* 获取活动列表.
*/
@GetMapping("/list")
ResultResponse<List<LotteryActivityDTO>> listActivities();
}

View File

@ -0,0 +1,49 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivitySaveCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 抽奖活动管理Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityManageClientApi {
String API_PREFIX = "/lottery-activity-manage/client";
/**
* 创建活动.
*/
@PostMapping("/create")
ResultResponse<Void> createActivity(@RequestBody LotteryActivitySaveCmd cmd);
/**
* 更新活动.
*/
@PutMapping("/update")
ResultResponse<Void> updateActivity(@RequestBody LotteryActivitySaveCmd cmd);
/**
* 更新活动状态.
*/
@PutMapping("/updateStatus")
ResultResponse<Void> updateActivityStatus(
@RequestParam("id") Long id,
@RequestParam("status") Integer status
);
/**
* 根据ID获取活动.
*/
@GetMapping("/getById")
ResultResponse<LotteryActivityDTO> getById(@RequestParam("id") Long id);
}

View File

@ -0,0 +1,26 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivityQryCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 活动查询Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityQueryClientApi {
String API_PREFIX = "/lottery-activity-query/client";
/**
* 分页查询活动列表.
*/
@GetMapping("/listActivities")
ResultResponse<PageResult<LotteryActivityDTO>> listActivities(@RequestBody LotteryActivityQryCmd cmd);
}

View File

@ -0,0 +1,37 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 奖品配置Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryPrizeClientApi {
String API_PREFIX = "/lottery-prize/client";
/**
* 根据活动ID获取奖品列表.
*/
@GetMapping("/listByActivityId")
ResultResponse<List<LotteryPrizeDTO>> listByActivityId(@RequestParam("activityId") Long activityId);
/**
* 根据奖品ID获取奖品信息.
*/
@GetMapping("/getById")
ResultResponse<LotteryPrizeDTO> getById(@RequestParam("id") Long id);
/**
* 扣减奖品库存.
*/
@GetMapping("/deductStock")
ResultResponse<Boolean> deductStock(@RequestParam("id") Long id, @RequestParam("count") Integer count);
}

View File

@ -0,0 +1,47 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.cmd.activity.LotteryPrizeSaveCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 奖品管理Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryPrizeManageClientApi {
String API_PREFIX = "/lottery-prize-manage/client";
/**
* 保存奖品新增或更新.
*/
@PostMapping("/save")
ResultResponse<Void> savePrize(@RequestBody LotteryPrizeSaveCmd cmd);
/**
* 根据活动ID获取奖品列表.
*/
@GetMapping("/listByActivityId")
ResultResponse<List<LotteryPrizeDTO>> listByActivityId(@RequestParam("activityId") Long activityId);
/**
* 删除奖品.
*/
@DeleteMapping("/delete")
ResultResponse<Void> deletePrize(@RequestParam("id") Long id);
/**
* 更新活动总库存.
*/
@PostMapping("/updateActivityStock")
ResultResponse<Void> updateActivityStock(@RequestParam("activityId") Long activityId);
}

View File

@ -0,0 +1,39 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 中奖记录Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryRecordClientApi {
String API_PREFIX = "/lottery-record/client";
/**
* 保存中奖记录.
*/
@PostMapping("/save")
ResultResponse<Long> saveRecord(
@RequestParam("recordNo") String recordNo,
@RequestParam("userId") Long userId,
@RequestParam("activityId") Long activityId,
@RequestParam(value = "prizeId", required = false) Long prizeId,
@RequestParam(value = "prizeName", required = false) String prizeName,
@RequestParam(value = "prizeType", required = false) Integer prizeType,
@RequestParam("isWin") Integer isWin
);
/**
* 根据记录编号获取记录.
*/
@GetMapping("/getByRecordNo")
ResultResponse<LotteryRecordDTO> getByRecordNo(@RequestParam("recordNo") String recordNo);
}

View File

@ -0,0 +1,34 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 中奖记录管理Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryRecordManageClientApi {
String API_PREFIX = "/lottery-record-manage/client";
/**
* 分页查询中奖记录.
*/
@GetMapping("/listRecords")
ResultResponse<PageResult<LotteryRecordDTO>> listRecords(@RequestBody LotteryRecordQryCmd cmd);
/**
* 发放奖品.
*/
@PostMapping("/deliverPrize")
ResultResponse<Void> deliverPrize(@RequestParam("recordId") Long recordId);
}

View File

@ -0,0 +1,44 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 抽奖券Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryTicketClientApi {
String API_PREFIX = "/lottery-ticket/client";
/**
* 获取用户抽奖券总数.
*/
@GetMapping("/getUserTicketCount")
ResultResponse<Integer> getUserTicketCount(@RequestParam("userId") Long userId);
/**
* 扣减用户抽奖券.
*/
@PostMapping("/deductTicket")
ResultResponse<Boolean> deductTicket(
@RequestParam("userId") Long userId,
@RequestParam("count") Integer count
);
/**
* 增加用户抽奖券.
*/
@PostMapping("/addTicket")
ResultResponse<Boolean> addTicket(
@RequestParam("userId") Long userId,
@RequestParam("count") Integer count,
@RequestParam(value = "source", required = false) String source,
@RequestParam(value = "sourceId", required = false) String sourceId
);
}

View File

@ -0,0 +1,24 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.inner.model.cmd.activity.LotteryTicketGrantCmd;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 抽奖券管理Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryTicketManageClientApi {
String API_PREFIX = "/lottery-ticket-manage/client";
/**
* 批量发放抽奖券.
*/
@PostMapping("/grantTickets")
ResultResponse<Void> grantTickets(@RequestBody LotteryTicketGrantCmd cmd);
}

View File

@ -0,0 +1,35 @@
package com.red.circle.other.inner.endpoint.activity.api;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 用户抽奖次数统计Client API.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryUserCountClientApi {
String API_PREFIX = "/lottery-user-count/client";
/**
* 获取用户今日抽奖次数.
*/
@GetMapping("/getTodayCount")
ResultResponse<Integer> getTodayCount(
@RequestParam("userId") Long userId,
@RequestParam("activityId") Long activityId
);
/**
* 增加用户抽奖次数.
*/
@GetMapping("/incrDrawCount")
ResultResponse<Void> incrDrawCount(
@RequestParam("userId") Long userId,
@RequestParam("activityId") Long activityId
);
}

View File

@ -0,0 +1,185 @@
package com.red.circle.other.inner.asserts.lottery;
import com.red.circle.framework.dto.IResponseErrorCode;
import com.red.circle.framework.dto.ResErrorCode;
/**
* 抽奖活动错误码: 9700 ~ 9900
*
* @author system
* @since 2025-10-18
*/
@ResErrorCode(describe = "抽奖活动Code", minCode = 9700, maxCode = 9900)
public enum LotteryErrorCode implements IResponseErrorCode {
/**
* 活动不存在.
*/
ACTIVITY_NOT_FOUND(9701, "Activity does not exist"),
/**
* 活动编码已存在.
*/
ACTIVITY_CODE_EXISTS(9702, "Activity code already exists"),
/**
* 活动未开始.
*/
ACTIVITY_NOT_STARTED(9703, "Activity has not started yet"),
/**
* 活动已结束.
*/
ACTIVITY_ENDED(9704, "Activity has ended"),
/**
* 活动已关闭.
*/
ACTIVITY_CLOSED(9705, "Activity is closed"),
/**
* 不在活动时间内.
*/
NOT_IN_ACTIVITY_TIME(9706, "Not within the activity time"),
/**
* 今日抽奖次数已用完.
*/
DRAW_COUNT_EXHAUSTED(9707, "Today's lottery draw limit has been reached"),
/**
* 抽奖券不足.
*/
INSUFFICIENT_TICKETS(9708, "Insufficient lottery tickets"),
/**
* 奖品不存在.
*/
PRIZE_NOT_FOUND(9709, "Prize does not exist"),
/**
* 奖品库存不足.
*/
PRIZE_STOCK_INSUFFICIENT(9710, "Prize stock is insufficient"),
/**
* 中奖记录不存在.
*/
RECORD_NOT_FOUND(9711, "Lottery record does not exist"),
/**
* 未中奖.
*/
NOT_WIN(9712, "Did not win a prize"),
/**
* 奖品已发放.
*/
PRIZE_ALREADY_DELIVERED(9713, "Prize has already been delivered"),
/**
* 奖品发放失败.
*/
PRIZE_DELIVERY_FAILED(9714, "Prize delivery failed"),
/**
* 抽奖券已失效.
*/
TICKET_EXPIRED(9715, "Lottery ticket has expired"),
/**
* 抽奖券状态异常.
*/
TICKET_STATUS_ABNORMAL(9716, "Lottery ticket status is abnormal"),
/**
* 用户抽奖券不足.
*/
USER_TICKET_INSUFFICIENT(9717, "User has insufficient lottery tickets"),
/**
* 活动总库存不足.
*/
ACTIVITY_STOCK_INSUFFICIENT(9718, "Activity total stock is insufficient"),
/**
* 奖品概率配置错误.
*/
PRIZE_PROBABILITY_ERROR(9719, "Prize probability configuration error"),
/**
* 活动状态不允许修改.
*/
ACTIVITY_STATUS_NOT_ALLOW_UPDATE(9720, "Activity status does not allow updates"),
/**
* 活动ID不能为空.
*/
ACTIVITY_ID_REQUIRED(9721, "Activity ID is required"),
/**
* 奖品ID不能为空.
*/
PRIZE_ID_REQUIRED(9722, "Prize ID is required"),
/**
* 记录编号不能为空.
*/
RECORD_NO_REQUIRED(9723, "Record number is required"),
/**
* 用户ID不能为空.
*/
USER_ID_REQUIRED(9724, "User ID is required"),
/**
* 发放数量必须大于0.
*/
GRANT_COUNT_INVALID(9725, "Grant count must be greater than 0"),
/**
* 抽奖失败,请稍后重试.
*/
DRAW_FAILED(9726, "Lottery draw failed, please try again later"),
/**
* 无可用奖品.
*/
NO_AVAILABLE_PRIZE(9727, "No available prize"),
/**
* 奖品扣减库存失败.
*/
PRIZE_DEDUCT_STOCK_FAILED(9728, "Failed to deduct prize stock"),
/**
* 用户抽奖次数更新失败.
*/
USER_DRAW_COUNT_UPDATE_FAILED(9729, "Failed to update user draw count"),
;
private final Integer code;
private final String message;
LotteryErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getMessage() {
return message;
}
@Override
public String getErrorCodeName() {
return this.name();
}
}

View File

@ -0,0 +1,53 @@
package com.red.circle.other.inner.model.cmd.activity;
import com.red.circle.framework.dto.Command;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 抽奖活动查询命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryActivityQryCmd extends Command {
/**
* 活动名称.
*/
private String activityName;
/**
* 活动编码.
*/
private String activityCode;
/**
* 状态.
*/
private Integer status;
/**
* 开始时间-.
*/
private LocalDateTime startTimeBegin;
/**
* 开始时间-.
*/
private LocalDateTime startTimeEnd;
/**
* 页码.
*/
private Integer pageNo;
/**
* 页大小.
*/
private Integer pageSize;
}

View File

@ -0,0 +1,80 @@
package com.red.circle.other.inner.model.cmd.activity;
import com.red.circle.framework.dto.Command;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 创建/更新活动命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryActivitySaveCmd extends Command {
/**
* 活动ID更新时需要.
*/
private Long id;
/**
* 活动编码.
*/
@NotBlank(message = "活动编码不能为空")
private String activityCode;
/**
* 活动名称.
*/
@NotBlank(message = "活动名称不能为空")
private String activityName;
/**
* 活动描述.
*/
private String activityDesc;
/**
* 开始时间.
*/
@NotNull(message = "开始时间不能为空")
private LocalDateTime startTime;
/**
* 结束时间.
*/
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
/**
* 抽奖次数限制.
*/
private Integer drawCountLimit;
/**
* 次数限制周期.
*/
private String drawCountCycle;
/**
* 是否需要抽奖券.
*/
private Integer needTicket;
/**
* 每次消耗抽奖券数量.
*/
private Integer ticketCost;
/**
* 排序.
*/
private Integer sortOrder;
}

View File

@ -0,0 +1,91 @@
package com.red.circle.other.inner.model.cmd.activity;
import com.red.circle.framework.dto.Command;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 奖品配置命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryPrizeSaveCmd extends Command {
/**
* 奖品ID更新时需要.
*/
private Long id;
/**
* 活动ID.
*/
@NotNull(message = "活动ID不能为空")
private Long activityId;
/**
* 奖品编码.
*/
@NotBlank(message = "奖品编码不能为空")
private String prizeCode;
/**
* 奖品名称.
*/
@NotBlank(message = "奖品名称不能为空")
private String prizeName;
/**
* 奖品类型.
*/
@NotNull(message = "奖品类型不能为空")
private Integer prizeType;
/**
* 奖品价值.
*/
private BigDecimal prizeValue;
/**
* 奖品图片.
*/
private String prizeImage;
/**
* 奖品等级.
*/
private Integer prizeLevel;
/**
* 总库存.
*/
@NotNull(message = "总库存不能为空")
private Integer totalStock;
/**
* 每日库存限制.
*/
private Integer dailyStock;
/**
* 中奖概率.
*/
@NotNull(message = "中奖概率不能为空")
private BigDecimal probability;
/**
* 排序.
*/
private Integer sortOrder;
/**
* 是否展示.
*/
private Integer isDisplay;
}

View File

@ -0,0 +1,63 @@
package com.red.circle.other.inner.model.cmd.activity;
import com.red.circle.framework.dto.Command;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 中奖记录查询命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryRecordQryCmd extends Command {
/**
* 用户ID.
*/
private Long userId;
/**
* 活动ID.
*/
private Long activityId;
/**
* 活动编码.
*/
private String activityCode;
/**
* 是否中奖.
*/
private Integer isWin;
/**
* 奖品状态.
*/
private Integer prizeStatus;
/**
* 抽奖时间-.
*/
private LocalDateTime drawTimeBegin;
/**
* 抽奖时间-.
*/
private LocalDateTime drawTimeEnd;
/**
* 页码.
*/
private Integer pageNo;
/**
* 页大小.
*/
private Integer pageSize;
}

View File

@ -0,0 +1,51 @@
package com.red.circle.other.inner.model.cmd.activity;
import com.red.circle.framework.dto.Command;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 发放抽奖券命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryTicketGrantCmd extends Command {
/**
* 用户ID列表.
*/
@NotNull(message = "用户ID列表不能为空")
private java.util.List<Long> userIds;
/**
* 发放数量.
*/
@NotNull(message = "发放数量不能为空")
private Integer count;
/**
* 来源.
*/
private String ticketSource;
/**
* 来源ID.
*/
private String sourceId;
/**
* 过期时间.
*/
private LocalDateTime expireTime;
/**
* 备注.
*/
private String remark;
}

View File

@ -0,0 +1,101 @@
package com.red.circle.other.inner.model.dto.activity;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 抽奖活动DTO.
*
* @author system
* @since 2025-10-18
*/
@Data
public class LotteryActivityDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 活动ID.
*/
private Long id;
/**
* 活动编码.
*/
private String activityCode;
/**
* 活动名称.
*/
private String activityName;
/**
* 活动描述.
*/
private String activityDesc;
/**
* 开始时间.
*/
private LocalDateTime startTime;
/**
* 结束时间.
*/
private LocalDateTime endTime;
/**
* 状态.
*/
private Integer status;
/**
* 总库存.
*/
private Integer totalStock;
/**
* 已消耗库存.
*/
private Integer consumedStock;
/**
* 抽奖次数限制.
*/
private Integer drawCountLimit;
/**
* 次数限制周期.
*/
private String drawCountCycle;
/**
* 是否需要抽奖券.
*/
private Integer needTicket;
/**
* 每次消耗抽奖券数量.
*/
private Integer ticketCost;
/**
* 排序.
*/
private Integer sortOrder;
/**
* 创建时间.
*/
private LocalDateTime createdAt;
/**
* 更新时间.
*/
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,101 @@
package com.red.circle.other.inner.model.dto.activity;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 奖品配置DTO.
*
* @author system
* @since 2025-10-18
*/
@Data
public class LotteryPrizeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 奖品ID.
*/
private Long id;
/**
* 活动ID.
*/
private Long activityId;
/**
* 奖品编码.
*/
private String prizeCode;
/**
* 奖品名称.
*/
private String prizeName;
/**
* 奖品类型.
*/
private Integer prizeType;
/**
* 奖品价值.
*/
private BigDecimal prizeValue;
/**
* 奖品图片.
*/
private String prizeImage;
/**
* 奖品等级.
*/
private Integer prizeLevel;
/**
* 总库存.
*/
private Integer totalStock;
/**
* 剩余库存.
*/
private Integer remainingStock;
/**
* 每日库存限制.
*/
private Integer dailyStock;
/**
* 中奖概率.
*/
private BigDecimal probability;
/**
* 排序.
*/
private Integer sortOrder;
/**
* 是否展示.
*/
private Integer isDisplay;
/**
* 创建时间.
*/
private LocalDateTime createdAt;
/**
* 更新时间.
*/
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,90 @@
package com.red.circle.other.inner.model.dto.activity;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 中奖记录DTO.
*
* @author system
* @since 2025-10-18
*/
@Data
public class LotteryRecordDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 记录ID.
*/
private Long id;
/**
* 记录编号.
*/
private String recordNo;
/**
* 用户ID.
*/
private Long userId;
/**
* 活动ID.
*/
private Long activityId;
/**
* 活动名称.
*/
private String activityName;
/**
* 奖品ID.
*/
private Long prizeId;
/**
* 奖品名称.
*/
private String prizeName;
/**
* 奖品类型.
*/
private Integer prizeType;
/**
* 是否中奖.
*/
private Integer isWin;
/**
* 抽奖时间.
*/
private LocalDateTime drawTime;
/**
* 奖品状态.
*/
private Integer prizeStatus;
/**
* 发放时间.
*/
private LocalDateTime deliverTime;
/**
* 备注.
*/
private String remark;
/**
* 创建时间.
*/
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,127 @@
package com.red.circle.console.adapter.app.activity;
import com.red.circle.console.app.service.app.activity.LotteryActivityBackService;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivityQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivitySaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryPrizeSaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryTicketGrantCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 抽奖活动管理.
*
* @author system on 2025-10-18
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/activity/lottery")
public class LotteryActivityBackRestController extends BaseController {
private final LotteryActivityBackService lotteryActivityBackService;
/**
* 创建活动.
*/
@PostMapping("/create")
public void createActivity(@RequestBody @Validated LotteryActivitySaveCmd cmd) {
lotteryActivityBackService.createActivity(cmd);
}
/**
* 更新活动.
*/
@PutMapping("/update")
public void updateActivity(@RequestBody @Validated LotteryActivitySaveCmd cmd) {
lotteryActivityBackService.updateActivity(cmd);
}
/**
* 活动列表.
*/
@GetMapping("/list")
public PageResult<LotteryActivityDTO> listActivities(LotteryActivityQryCmd cmd) {
return lotteryActivityBackService.listActivities(cmd);
}
/**
* 活动详情.
*/
@GetMapping("/detail/{id}")
public LotteryActivityDTO getActivity(@PathVariable Long id) {
return lotteryActivityBackService.getActivity(id);
}
/**
* 更新活动状态.
*/
@PutMapping("/status/{id}")
public void updateActivityStatus(@PathVariable Long id, @RequestParam Integer status) {
lotteryActivityBackService.updateActivityStatus(id, status);
}
/**
* 配置奖品.
*/
@PostMapping("/prize/save")
public void savePrize(@RequestBody @Validated LotteryPrizeSaveCmd cmd) {
lotteryActivityBackService.savePrize(cmd);
}
/**
* 奖品列表.
*/
@GetMapping("/prize/list/{activityId}")
public List<LotteryPrizeDTO> listPrizes(@PathVariable Long activityId) {
return lotteryActivityBackService.listPrizes(activityId);
}
/**
* 删除奖品.
*/
@DeleteMapping("/prize/delete/{id}")
public void deletePrize(@PathVariable Long id) {
lotteryActivityBackService.deletePrize(id);
}
/**
* 中奖记录列表.
*/
@GetMapping("/record/list")
public PageResult<LotteryRecordDTO> listRecords(LotteryRecordQryCmd cmd) {
return lotteryActivityBackService.listRecords(cmd);
}
/**
* 发放奖品.
*/
@PostMapping("/record/deliver/{recordId}")
public void deliverPrize(@PathVariable Long recordId) {
lotteryActivityBackService.deliverPrize(recordId);
}
/**
* 批量发放抽奖券.
*/
@PostMapping("/ticket/grant")
public void grantTickets(@RequestBody @Validated LotteryTicketGrantCmd cmd) {
lotteryActivityBackService.grantTickets(cmd);
}
}

View File

@ -0,0 +1,92 @@
package com.red.circle.console.app.service.app.activity;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.endpoint.activity.LotteryActivityManageClient;
import com.red.circle.other.inner.endpoint.activity.LotteryActivityQueryClient;
import com.red.circle.other.inner.endpoint.activity.LotteryPrizeManageClient;
import com.red.circle.other.inner.endpoint.activity.LotteryRecordManageClient;
import com.red.circle.other.inner.endpoint.activity.LotteryTicketManageClient;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivityQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivitySaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryPrizeSaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryTicketGrantCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 抽奖活动管理服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
@RequiredArgsConstructor
public class LotteryActivityBackServiceImpl implements LotteryActivityBackService {
private final LotteryActivityManageClient lotteryActivityManageClient;
private final LotteryActivityQueryClient lotteryActivityQueryClient;
private final LotteryPrizeManageClient lotteryPrizeManageClient;
private final LotteryRecordManageClient lotteryRecordManageClient;
private final LotteryTicketManageClient lotteryTicketManageClient;
@Override
public void createActivity(LotteryActivitySaveCmd cmd) {
lotteryActivityManageClient.createActivity(cmd);
}
@Override
public void updateActivity(LotteryActivitySaveCmd cmd) {
lotteryActivityManageClient.updateActivity(cmd);
}
@Override
public PageResult<LotteryActivityDTO> listActivities(LotteryActivityQryCmd cmd) {
return lotteryActivityQueryClient.listActivities(cmd).getBody();
}
@Override
public LotteryActivityDTO getActivity(Long id) {
return lotteryActivityManageClient.getById(id).getBody();
}
@Override
public void updateActivityStatus(Long id, Integer status) {
lotteryActivityManageClient.updateActivityStatus(id, status);
}
@Override
public void savePrize(LotteryPrizeSaveCmd cmd) {
lotteryPrizeManageClient.savePrize(cmd);
}
@Override
public List<LotteryPrizeDTO> listPrizes(Long activityId) {
return lotteryPrizeManageClient.listByActivityId(activityId).getBody();
}
@Override
public void deletePrize(Long id) {
lotteryPrizeManageClient.deletePrize(id);
}
@Override
public PageResult<LotteryRecordDTO> listRecords(LotteryRecordQryCmd cmd) {
return lotteryRecordManageClient.listRecords(cmd).getBody();
}
@Override
public void deliverPrize(Long recordId) {
lotteryRecordManageClient.deliverPrize(recordId);
}
@Override
public void grantTickets(LotteryTicketGrantCmd cmd) {
lotteryTicketManageClient.grantTickets(cmd);
}
}

View File

@ -0,0 +1,77 @@
package com.red.circle.console.app.service.app.activity;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivityQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivitySaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryPrizeSaveCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.inner.model.cmd.activity.LotteryTicketGrantCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.util.List;
/**
* 抽奖活动管理服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityBackService {
/**
* 创建活动.
*/
void createActivity(LotteryActivitySaveCmd cmd);
/**
* 更新活动.
*/
void updateActivity(LotteryActivitySaveCmd cmd);
/**
* 活动列表.
*/
PageResult<LotteryActivityDTO> listActivities(LotteryActivityQryCmd cmd);
/**
* 活动详情.
*/
LotteryActivityDTO getActivity(Long id);
/**
* 启用/禁用活动.
*/
void updateActivityStatus(Long id, Integer status);
/**
* 配置奖品.
*/
void savePrize(LotteryPrizeSaveCmd cmd);
/**
* 奖品列表.
*/
List<LotteryPrizeDTO> listPrizes(Long activityId);
/**
* 删除奖品.
*/
void deletePrize(Long id);
/**
* 中奖记录列表.
*/
PageResult<LotteryRecordDTO> listRecords(LotteryRecordQryCmd cmd);
/**
* 发放奖品.
*/
void deliverPrize(Long recordId);
/**
* 批量发放抽奖券.
*/
void grantTickets(LotteryTicketGrantCmd cmd);
}

View File

@ -0,0 +1,123 @@
package com.red.circle.other.adapter.app.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.app.service.activity.LotteryActivityService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 抽奖活动 前端控制器.
* </p>
*
* @author system
* @eo.api-type http
* @eo.groupName 活动服务.抽奖活动
* @eo.path /activity/lottery
* @since 2025-10-18
*/
@Slf4j
@RestController
@RequestMapping(value = "/activity/lottery", produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryActivityRestController extends BaseController {
private final LotteryActivityService lotteryActivityService;
/**
* 获取有效活动列表.
*
* @eo.name 获取有效活动列表.
* @eo.url /list
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/list")
public List<LotteryActivityCO> listValidActivities(AppExtCommand cmd) {
return lotteryActivityService.listValidActivities(cmd);
}
/**
* 获取活动详情.
*
* @eo.name 获取活动详情.
* @eo.url /detail/{activityCode}
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/detail/{activityCode}")
public LotteryActivityDetailCO getActivityDetail(@PathVariable String activityCode, AppExtCommand cmd) {
return lotteryActivityService.getActivityDetail(activityCode, cmd);
}
/**
* 执行抽奖.
*
* @eo.name 执行抽奖.
* @eo.url /draw
* @eo.method post
* @eo.request-type json
*/
@PostMapping("/draw")
public LotteryDrawResultCO draw(@RequestBody @Validated LotteryDrawCmd cmd) {
return lotteryActivityService.draw(cmd);
}
/**
* 查询我的中奖记录.
*
* @eo.name 查询我的中奖记录.
* @eo.url /my-records
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/my-records")
public PageResult<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd) {
return lotteryActivityService.listMyRecords(cmd);
}
/**
* 查询我的抽奖券.
*
* @eo.name 查询我的抽奖券.
* @eo.url /my-tickets
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/my-tickets")
public List<LotteryTicketCO> listMyTickets(AppExtCommand cmd) {
return lotteryActivityService.listMyTickets(cmd);
}
/**
* 获取抽奖券总数.
*
* @eo.name 获取抽奖券总数.
* @eo.url /ticket-count
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/ticket-count")
public Integer getMyTicketCount(AppExtCommand cmd) {
return lotteryActivityService.getMyTicketCount(cmd);
}
}

View File

@ -0,0 +1,127 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryPrizeCO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询活动详情.
*
* @author system
* @since 2025-10-18
*/
@Component
@RequiredArgsConstructor
public class LotteryActivityDetailQryExe {
private final LotteryActivityService lotteryActivityService;
private final LotteryPrizeService lotteryPrizeService;
private final LotteryTicketService lotteryTicketService;
private final LotteryUserCountService lotteryUserCountService;
public LotteryActivityDetailCO execute(String activityCode, AppExtCommand cmd) {
Long userId = cmd.requiredReqUserId();
// 查询活动
LotteryActivity activity = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, activityCode)
.one();
ResponseAssert.isTrue(activity != null, "活动不存在");
// 查询奖品列表
List<LotteryPrize> prizes = lotteryPrizeService.lambdaQuery()
.eq(LotteryPrize::getActivityId, activity.getId())
.eq(LotteryPrize::getIsDisplay, 1)
.orderByAsc(LotteryPrize::getSortOrder)
.list();
// 查询用户抽奖券数量
Integer ticketCount = 0;
if (activity.getNeedTicket() == 1) {
ticketCount = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.list()
.stream()
.mapToInt(LotteryTicket::getRemainingCount)
.sum();
}
// 查询用户今日抽奖次数
Integer usedCount = 0;
if (activity.getDrawCountLimit() != null && activity.getDrawCountLimit() > 0) {
LotteryUserCount userCount = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.eq(LotteryUserCount::getActivityId, activity.getId())
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.one();
if (userCount != null) {
usedCount = userCount.getDrawCount();
}
}
// 组装返回数据
LotteryActivityDetailCO detail = new LotteryActivityDetailCO();
detail.setActivity(convertActivityToCO(activity, usedCount));
detail.setPrizeList(prizes.stream()
.map(this::convertPrizeToCO)
.collect(Collectors.toList()));
detail.setUserTicketCount(ticketCount);
return detail;
}
private LotteryActivityCO convertActivityToCO(LotteryActivity activity, Integer usedCount) {
LotteryActivityCO co = new LotteryActivityCO();
co.setId(activity.getId());
co.setActivityCode(activity.getActivityCode());
co.setActivityName(activity.getActivityName());
co.setActivityDesc(activity.getActivityDesc());
co.setStartTime(activity.getStartTime());
co.setEndTime(activity.getEndTime());
co.setStatus(activity.getStatus());
co.setNeedTicket(activity.getNeedTicket());
co.setTicketCost(activity.getTicketCost());
co.setDrawCountLimit(activity.getDrawCountLimit());
co.setDrawCountCycle(activity.getDrawCountCycle());
if (activity.getDrawCountLimit() != null && activity.getDrawCountLimit() > 0) {
co.setUserRemainingCount(Math.max(0, activity.getDrawCountLimit() - usedCount));
} else {
co.setUserRemainingCount(-1);
}
return co;
}
private LotteryPrizeCO convertPrizeToCO(LotteryPrize prize) {
LotteryPrizeCO co = new LotteryPrizeCO();
co.setId(prize.getId());
co.setPrizeCode(prize.getPrizeCode());
co.setPrizeName(prize.getPrizeName());
co.setPrizeType(prize.getPrizeType());
co.setPrizeValue(prize.getPrizeValue());
co.setPrizeImage(prize.getPrizeImage());
co.setPrizeLevel(prize.getPrizeLevel());
co.setSortOrder(prize.getSortOrder());
return co;
}
}

View File

@ -0,0 +1,93 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询有效活动列表.
*
* @author system
* @since 2025-10-18
*/
@Component
@RequiredArgsConstructor
public class LotteryActivityListQryExe {
private final LotteryActivityService lotteryActivityService;
private final LotteryUserCountService lotteryUserCountService;
public List<LotteryActivityCO> execute(AppExtCommand cmd) {
Long userId = cmd.requiredReqUserId();
LocalDateTime now = LocalDateTime.now();
// 查询进行中的活动
List<LotteryActivity> activities = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getStatus, 1)
.le(LotteryActivity::getStartTime, now)
.ge(LotteryActivity::getEndTime, now)
.orderByAsc(LotteryActivity::getSortOrder)
.list();
if (activities.isEmpty()) {
return List.of();
}
// 查询用户今日抽奖次数
List<Long> activityIds = activities.stream()
.map(LotteryActivity::getId)
.collect(Collectors.toList());
Map<Long, Integer> userDrawCountMap = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.in(LotteryUserCount::getActivityId, activityIds)
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.list()
.stream()
.collect(Collectors.toMap(
LotteryUserCount::getActivityId,
LotteryUserCount::getDrawCount
));
// 转换为CO
return activities.stream()
.map(activity -> convertToCO(activity, userDrawCountMap))
.collect(Collectors.toList());
}
private LotteryActivityCO convertToCO(LotteryActivity activity, Map<Long, Integer> userDrawCountMap) {
LotteryActivityCO co = new LotteryActivityCO();
co.setId(activity.getId());
co.setActivityCode(activity.getActivityCode());
co.setActivityName(activity.getActivityName());
co.setActivityDesc(activity.getActivityDesc());
co.setStartTime(activity.getStartTime());
co.setEndTime(activity.getEndTime());
co.setStatus(activity.getStatus());
co.setNeedTicket(activity.getNeedTicket());
co.setTicketCost(activity.getTicketCost());
co.setDrawCountLimit(activity.getDrawCountLimit());
co.setDrawCountCycle(activity.getDrawCountCycle());
// 计算剩余次数
if (activity.getDrawCountLimit() != null && activity.getDrawCountLimit() > 0) {
int usedCount = userDrawCountMap.getOrDefault(activity.getId(), 0);
co.setUserRemainingCount(Math.max(0, activity.getDrawCountLimit() - usedCount));
} else {
co.setUserRemainingCount(-1); // -1表示不限制
}
return co;
}
}

View File

@ -0,0 +1,253 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryPrizeCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 执行抽奖.
*
* @author system
* @since 2025-10-18
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LotteryDrawExe {
private final LotteryActivityService lotteryActivityService;
private final LotteryPrizeService lotteryPrizeService;
private final LotteryRecordService lotteryRecordService;
private final LotteryTicketService lotteryTicketService;
private final LotteryTicketRecordService lotteryTicketRecordService;
private final LotteryUserCountService lotteryUserCountService;
private final Random random = new Random();
@Transactional(rollbackFor = Exception.class)
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
Long userId = cmd.requiredReqUserId();
String activityCode = cmd.getActivityCode();
// 1. 校验活动
LotteryActivity activity = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, activityCode)
.one();
ResponseAssert.isTrue(activity != null, "活动不存在");
ResponseAssert.isTrue(activity.getStatus() == 1, "活动未开始或已结束");
LocalDateTime now = LocalDateTime.now();
ResponseAssert.isTrue(now.isAfter(activity.getStartTime()) && now.isBefore(activity.getEndTime()),
"不在活动时间内");
// 2. 校验抽奖次数
if (activity.getDrawCountLimit() != null && activity.getDrawCountLimit() > 0) {
LotteryUserCount userCount = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.eq(LotteryUserCount::getActivityId, activity.getId())
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.one();
int drawCount = userCount != null ? userCount.getDrawCount() : 0;
ResponseAssert.isTrue(drawCount < activity.getDrawCountLimit(), "今日抽奖次数已用完");
}
// 3. 校验并扣除抽奖券
if (activity.getNeedTicket() == 1) {
deductTicket(userId, activity.getTicketCost());
}
// 4. 执行抽奖算法
LotteryPrize prize = drawPrize(activity.getId());
// 5. 保存抽奖记录
String recordNo = generateRecordNo();
LotteryRecord record = new LotteryRecord();
record.setRecordNo(recordNo);
record.setUserId(userId);
record.setActivityId(activity.getId());
record.setDrawTime(now);
if (prize != null) {
record.setPrizeId(prize.getId());
record.setPrizeName(prize.getPrizeName());
record.setPrizeType(prize.getPrizeType());
record.setIsWin(1);
record.setPrizeStatus(0); // 待发放
// 扣减库存
lotteryPrizeService.lambdaUpdate()
.eq(LotteryPrize::getId, prize.getId())
.gt(LotteryPrize::getRemainingStock, 0)
.setSql("remaining_stock = remaining_stock - 1")
.update();
} else {
record.setIsWin(0);
}
lotteryRecordService.save(record);
// 6. 更新用户抽奖次数
updateUserDrawCount(userId, activity.getId());
// 7. 组装返回结果
return convertToResultCO(record, prize);
}
/**
* 扣除抽奖券.
*/
private void deductTicket(Long userId, Integer ticketCost) {
List<LotteryTicket> tickets = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.orderByAsc(LotteryTicket::getExpireTime)
.list();
int totalRemaining = tickets.stream()
.mapToInt(LotteryTicket::getRemainingCount)
.sum();
ResponseAssert.isTrue(totalRemaining >= ticketCost, "抽奖券不足");
int remaining = ticketCost;
for (LotteryTicket ticket : tickets) {
if (remaining <= 0) {
break;
}
int deduct = Math.min(remaining, ticket.getRemainingCount());
lotteryTicketService.lambdaUpdate()
.eq(LotteryTicket::getId, ticket.getId())
.setSql("used_count = used_count + " + deduct)
.setSql("remaining_count = remaining_count - " + deduct)
.update();
// 记录使用
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
ticketRecord.setUserId(userId);
ticketRecord.setTicketId(ticket.getId());
ticketRecord.setChangeCount(-deduct);
ticketRecord.setChangeType(2); // 消耗
ticketRecord.setRemark("抽奖消耗");
lotteryTicketRecordService.save(ticketRecord);
remaining -= deduct;
}
}
/**
* 抽奖算法 - 权重随机.
*/
private LotteryPrize drawPrize(Long activityId) {
// 查询所有可用奖品
List<LotteryPrize> prizes = lotteryPrizeService.lambdaQuery()
.eq(LotteryPrize::getActivityId, activityId)
.gt(LotteryPrize::getRemainingStock, 0)
.list();
if (prizes.isEmpty()) {
return null;
}
// 计算总概率
BigDecimal totalProbability = prizes.stream()
.map(LotteryPrize::getProbability)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 生成随机数
double randomValue = random.nextDouble() * totalProbability.doubleValue();
double cumulative = 0.0;
// 根据概率选择奖品
for (LotteryPrize prize : prizes) {
cumulative += prize.getProbability().doubleValue();
if (randomValue <= cumulative) {
return prize;
}
}
return prizes.get(prizes.size() - 1);
}
/**
* 更新用户抽奖次数.
*/
private void updateUserDrawCount(Long userId, Long activityId) {
LotteryUserCount userCount = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.eq(LotteryUserCount::getActivityId, activityId)
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.one();
if (userCount == null) {
userCount = new LotteryUserCount();
userCount.setUserId(userId);
userCount.setActivityId(activityId);
userCount.setDrawDate(LocalDate.now());
userCount.setDrawCount(1);
lotteryUserCountService.save(userCount);
} else {
lotteryUserCountService.lambdaUpdate()
.eq(LotteryUserCount::getId, userCount.getId())
.setSql("draw_count = draw_count + 1")
.update();
}
}
/**
* 生成记录编号.
*/
private String generateRecordNo() {
return "LT" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8);
}
/**
* 转换为结果CO.
*/
private LotteryDrawResultCO convertToResultCO(LotteryRecord record, LotteryPrize prize) {
LotteryDrawResultCO result = new LotteryDrawResultCO();
result.setRecordNo(record.getRecordNo());
result.setIsWin(record.getIsWin() == 1);
result.setDrawTime(record.getDrawTime());
if (prize != null) {
LotteryPrizeCO prizeCO = new LotteryPrizeCO();
prizeCO.setId(prize.getId());
prizeCO.setPrizeCode(prize.getPrizeCode());
prizeCO.setPrizeName(prize.getPrizeName());
prizeCO.setPrizeType(prize.getPrizeType());
prizeCO.setPrizeValue(prize.getPrizeValue());
prizeCO.setPrizeImage(prize.getPrizeImage());
prizeCO.setPrizeLevel(prize.getPrizeLevel());
result.setPrize(prizeCO);
}
return result;
}
}

View File

@ -0,0 +1,123 @@
package com.red.circle.other.app.command.activity;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.activity.LotteryPrizeCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询我的抽奖记录.
*
* @author system
* @since 2025-10-18
*/
@Component
@RequiredArgsConstructor
public class LotteryRecordQryExe {
private final LotteryRecordService lotteryRecordService;
private final LotteryActivityService lotteryActivityService;
private final LotteryPrizeService lotteryPrizeService;
public PageResult<LotteryRecordCO> execute(LotteryRecordQryCmd cmd) {
Long userId = cmd.requiredReqUserId();
Integer pageNo = cmd.getPageNo() != null ? cmd.getPageNo() : 1;
Integer pageSize = cmd.getPageSize() != null ? cmd.getPageSize() : 20;
// 构建查询条件
Page<LotteryRecord> page = new Page<>(pageNo, pageSize);
var query = lotteryRecordService.lambdaQuery()
.eq(LotteryRecord::getUserId, userId);
if (cmd.getActivityCode() != null) {
LotteryActivity activity = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, cmd.getActivityCode())
.one();
if (activity != null) {
query.eq(LotteryRecord::getActivityId, activity.getId());
}
}
if (cmd.getOnlyWin() != null && cmd.getOnlyWin()) {
query.eq(LotteryRecord::getIsWin, 1);
}
Page<LotteryRecord> recordPage = query.orderByDesc(LotteryRecord::getDrawTime).page(page);
// 查询关联数据
List<Long> activityIds = recordPage.getRecords().stream()
.map(LotteryRecord::getActivityId)
.distinct()
.collect(Collectors.toList());
List<Long> prizeIds = recordPage.getRecords().stream()
.map(LotteryRecord::getPrizeId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
Map<Long, LotteryActivity> activityMap = lotteryActivityService.lambdaQuery()
.in(LotteryActivity::getId, activityIds)
.list()
.stream()
.collect(Collectors.toMap(LotteryActivity::getId, a -> a));
Map<Long, LotteryPrize> prizeMap = lotteryPrizeService.lambdaQuery()
.in(LotteryPrize::getId, prizeIds)
.list()
.stream()
.collect(Collectors.toMap(LotteryPrize::getId, p -> p));
// 转换为CO
List<LotteryRecordCO> records = recordPage.getRecords().stream()
.map(record -> convertToCO(record, activityMap, prizeMap))
.collect(Collectors.toList());
return PageResult.of(records, recordPage.getTotal(), pageNo, pageSize);
}
private LotteryRecordCO convertToCO(LotteryRecord record,
Map<Long, LotteryActivity> activityMap,
Map<Long, LotteryPrize> prizeMap) {
LotteryRecordCO co = new LotteryRecordCO();
co.setRecordNo(record.getRecordNo());
co.setIsWin(record.getIsWin() == 1);
co.setDrawTime(record.getDrawTime());
co.setPrizeStatus(record.getPrizeStatus());
LotteryActivity activity = activityMap.get(record.getActivityId());
if (activity != null) {
co.setActivityName(activity.getActivityName());
}
if (record.getPrizeId() != null) {
LotteryPrize prize = prizeMap.get(record.getPrizeId());
if (prize != null) {
LotteryPrizeCO prizeCO = new LotteryPrizeCO();
prizeCO.setId(prize.getId());
prizeCO.setPrizeCode(prize.getPrizeCode());
prizeCO.setPrizeName(prize.getPrizeName());
prizeCO.setPrizeType(prize.getPrizeType());
prizeCO.setPrizeValue(prize.getPrizeValue());
prizeCO.setPrizeImage(prize.getPrizeImage());
prizeCO.setPrizeLevel(prize.getPrizeLevel());
co.setPrize(prizeCO);
}
}
return co;
}
}

View File

@ -0,0 +1,36 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询我的抽奖券总数.
*
* @author system
* @since 2025-10-18
*/
@Component
@RequiredArgsConstructor
public class LotteryTicketCountQryExe {
private final LotteryTicketService lotteryTicketService;
public Integer execute(AppExtCommand cmd) {
Long userId = cmd.requiredReqUserId();
List<LotteryTicket> tickets = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.list();
return tickets.stream()
.mapToInt(LotteryTicket::getRemainingCount)
.sum();
}
}

View File

@ -0,0 +1,51 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询我的抽奖券.
*
* @author system
* @since 2025-10-18
*/
@Component
@RequiredArgsConstructor
public class LotteryTicketQryExe {
private final LotteryTicketService lotteryTicketService;
public List<LotteryTicketCO> execute(AppExtCommand cmd) {
Long userId = cmd.requiredReqUserId();
List<LotteryTicket> tickets = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.orderByAsc(LotteryTicket::getExpireTime)
.list();
return tickets.stream()
.map(this::convertToCO)
.collect(Collectors.toList());
}
private LotteryTicketCO convertToCO(LotteryTicket ticket) {
LotteryTicketCO co = new LotteryTicketCO();
co.setId(ticket.getId());
co.setTicketSource(ticket.getTicketSource());
co.setTotalCount(ticket.getTotalCount());
co.setUsedCount(ticket.getUsedCount());
co.setRemainingCount(ticket.getRemainingCount());
co.setExpireTime(ticket.getExpireTime());
co.setStatus(ticket.getStatus());
return co;
}
}

View File

@ -0,0 +1,69 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.command.activity.LotteryActivityDetailQryExe;
import com.red.circle.other.app.command.activity.LotteryActivityListQryExe;
import com.red.circle.other.app.command.activity.LotteryDrawExe;
import com.red.circle.other.app.command.activity.LotteryRecordQryExe;
import com.red.circle.other.app.command.activity.LotteryTicketCountQryExe;
import com.red.circle.other.app.command.activity.LotteryTicketQryExe;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 抽奖活动服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
@RequiredArgsConstructor
public class LotteryActivityServiceImpl implements LotteryActivityService {
private final LotteryActivityListQryExe lotteryActivityListQryExe;
private final LotteryActivityDetailQryExe lotteryActivityDetailQryExe;
private final LotteryDrawExe lotteryDrawExe;
private final LotteryRecordQryExe lotteryRecordQryExe;
private final LotteryTicketQryExe lotteryTicketQryExe;
private final LotteryTicketCountQryExe lotteryTicketCountQryExe;
@Override
public List<LotteryActivityCO> listValidActivities(AppExtCommand cmd) {
return lotteryActivityListQryExe.execute(cmd);
}
@Override
public LotteryActivityDetailCO getActivityDetail(String activityCode, AppExtCommand cmd) {
return lotteryActivityDetailQryExe.execute(activityCode, cmd);
}
@Override
public LotteryDrawResultCO draw(LotteryDrawCmd cmd) {
return lotteryDrawExe.execute(cmd);
}
@Override
public PageResult<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd) {
return lotteryRecordQryExe.execute(cmd);
}
@Override
public List<LotteryTicketCO> listMyTickets(AppExtCommand cmd) {
return lotteryTicketQryExe.execute(cmd);
}
@Override
public Integer getMyTicketCount(AppExtCommand cmd) {
return lotteryTicketCountQryExe.execute(cmd);
}
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.command.activity.LotteryDrawExe;
import com.red.circle.other.app.command.activity.query.LotteryActivityDetailQryExe;
import com.red.circle.other.app.command.activity.query.LotteryActivityListQryExe;
import com.red.circle.other.app.command.activity.query.LotteryRecordQryExe;
import com.red.circle.other.app.command.activity.query.LotteryTicketQryExe;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 抽奖服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
@RequiredArgsConstructor
public class LotteryServiceImpl implements LotteryService {
private final LotteryActivityListQryExe lotteryActivityListQryExe;
private final LotteryActivityDetailQryExe lotteryActivityDetailQryExe;
private final LotteryDrawExe lotteryDrawExe;
private final LotteryRecordQryExe lotteryRecordQryExe;
private final LotteryTicketQryExe lotteryTicketQryExe;
@Override
public List<LotteryActivityCO> listActiveActivities(AppExtCommand cmd) {
return lotteryActivityListQryExe.execute(cmd);
}
@Override
public LotteryActivityDetailCO getActivityDetail(String activityCode, AppExtCommand cmd) {
return lotteryActivityDetailQryExe.execute(activityCode, cmd);
}
@Override
public LotteryDrawResultCO draw(LotteryDrawCmd cmd) {
return lotteryDrawExe.execute(cmd);
}
@Override
public List<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd) {
return lotteryRecordQryExe.execute(cmd);
}
@Override
public List<LotteryTicketCO> listMyTickets(AppExtCommand cmd) {
return lotteryTicketQryExe.execute(cmd);
}
}

View File

@ -0,0 +1,80 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 抽奖活动CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryActivityCO extends ClientObject {
/**
* 活动ID.
*/
private Long id;
/**
* 活动编码.
*/
private String activityCode;
/**
* 活动名称.
*/
private String activityName;
/**
* 活动描述.
*/
private String activityDesc;
/**
* 开始时间.
*/
private LocalDateTime startTime;
/**
* 结束时间.
*/
private LocalDateTime endTime;
/**
* 状态0-未开始1-进行中2-已结束3-已关闭.
*/
private Integer status;
/**
* 是否需要抽奖券.
*/
private Integer needTicket;
/**
* 每次消耗抽奖券数量.
*/
private Integer ticketCost;
/**
* 每人抽奖次数限制.
*/
private Integer drawCountLimit;
/**
* 次数限制周期.
*/
private String drawCountCycle;
/**
* 用户剩余抽奖次数.
*/
private Integer userRemainingCount;
}

View File

@ -0,0 +1,35 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 抽奖活动详情CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryActivityDetailCO extends ClientObject {
/**
* 活动信息.
*/
private LotteryActivityCO activity;
/**
* 奖品列表.
*/
private List<LotteryPrizeCO> prizeList;
/**
* 用户抽奖券数量.
*/
private Integer userTicketCount;
}

View File

@ -0,0 +1,40 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 抽奖结果CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryDrawResultCO extends ClientObject {
/**
* 记录编号.
*/
private String recordNo;
/**
* 是否中奖.
*/
private Boolean isWin;
/**
* 中奖奖品信息.
*/
private LotteryPrizeCO prize;
/**
* 抽奖时间.
*/
private LocalDateTime drawTime;
}

View File

@ -0,0 +1,60 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 奖品CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryPrizeCO extends ClientObject {
/**
* 奖品ID.
*/
private Long id;
/**
* 奖品编码.
*/
private String prizeCode;
/**
* 奖品名称.
*/
private String prizeName;
/**
* 奖品类型.
*/
private Integer prizeType;
/**
* 奖品价值.
*/
private BigDecimal prizeValue;
/**
* 奖品图片.
*/
private String prizeImage;
/**
* 奖品等级.
*/
private Integer prizeLevel;
/**
* 排序九宫格位置.
*/
private Integer sortOrder;
}

View File

@ -0,0 +1,50 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 抽奖记录CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryRecordCO extends ClientObject {
/**
* 记录编号.
*/
private String recordNo;
/**
* 活动名称.
*/
private String activityName;
/**
* 奖品信息.
*/
private LotteryPrizeCO prize;
/**
* 是否中奖.
*/
private Boolean isWin;
/**
* 抽奖时间.
*/
private LocalDateTime drawTime;
/**
* 奖品状态0-待发放1-已发放2-发放失败.
*/
private Integer prizeStatus;
}

View File

@ -0,0 +1,55 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.red.circle.framework.dto.ClientObject;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户抽奖券CO.
*
* @author system
* @since 2025-10-18
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class LotteryTicketCO extends ClientObject {
/**
* 抽奖券ID.
*/
private Long id;
/**
* 来源.
*/
private String ticketSource;
/**
* 总数量.
*/
private Integer totalCount;
/**
* 已使用数量.
*/
private Integer usedCount;
/**
* 剩余数量.
*/
private Integer remainingCount;
/**
* 过期时间.
*/
private LocalDateTime expireTime;
/**
* 状态.
*/
private Integer status;
}

View File

@ -0,0 +1,26 @@
package com.red.circle.other.app.dto.cmd.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 执行抽奖Cmd.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryDrawCmd extends AppExtCommand {
/**
* 活动编码.
*
* @eo.required
*/
@NotBlank(message = "activityCode required.")
private String activityCode;
}

View File

@ -0,0 +1,37 @@
package com.red.circle.other.app.dto.cmd.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 抽奖记录查询命令.
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LotteryRecordQryCmd extends AppExtCommand {
/**
* 活动编码.
*/
private String activityCode;
/**
* 是否只查询中奖记录.
*/
private Boolean onlyWin;
/**
* 页码.
*/
private Integer pageNo;
/**
* 页大小.
*/
private Integer pageSize;
}

View File

@ -0,0 +1,52 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import java.util.List;
/**
* 抽奖活动服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityService {
/**
* 获取有效活动列表.
*/
List<LotteryActivityCO> listValidActivities(AppExtCommand cmd);
/**
* 获取活动详情.
*/
LotteryActivityDetailCO getActivityDetail(String activityCode, AppExtCommand cmd);
/**
* 执行抽奖.
*/
LotteryDrawResultCO draw(LotteryDrawCmd cmd);
/**
* 查询我的中奖记录.
*/
PageResult<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd);
/**
* 查询我的抽奖券.
*/
List<LotteryTicketCO> listMyTickets(AppExtCommand cmd);
/**
* 获取抽奖券总数.
*/
Integer getMyTicketCount(AppExtCommand cmd);
}

View File

@ -0,0 +1,46 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryActivityDetailCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryDrawResultCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.LotteryTicketCO;
import com.red.circle.other.app.dto.cmd.activity.LotteryDrawCmd;
import com.red.circle.other.app.dto.cmd.activity.LotteryRecordQryCmd;
import java.util.List;
/**
* 抽奖服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryService {
/**
* 获取有效活动列表.
*/
List<LotteryActivityCO> listActiveActivities(AppExtCommand cmd);
/**
* 获取活动详情.
*/
LotteryActivityDetailCO getActivityDetail(String activityCode, AppExtCommand cmd);
/**
* 执行抽奖.
*/
LotteryDrawResultCO draw(LotteryDrawCmd cmd);
/**
* 查询用户抽奖记录.
*/
List<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd);
/**
* 查询用户抽奖券.
*/
List<LotteryTicketCO> listMyTickets(AppExtCommand cmd);
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import org.apache.ibatis.annotations.Mapper;
/**
* 抽奖活动Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryActivityMapper extends BaseMapper<LotteryActivity> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import org.apache.ibatis.annotations.Mapper;
/**
* 奖品配置Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryPrizeMapper extends BaseMapper<LotteryPrize> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import org.apache.ibatis.annotations.Mapper;
/**
* 中奖记录Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryRecordMapper extends BaseMapper<LotteryRecord> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import org.apache.ibatis.annotations.Mapper;
/**
* 抽奖券Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryTicketMapper extends BaseMapper<LotteryTicket> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import org.apache.ibatis.annotations.Mapper;
/**
* 抽奖券使用记录Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryTicketRecordMapper extends BaseMapper<LotteryTicketRecord> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户抽奖次数统计Mapper.
*
* @author system
* @since 2025-10-18
*/
@Mapper
public interface LotteryUserCountMapper extends BaseMapper<LotteryUserCount> {
}

View File

@ -0,0 +1,114 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 抽奖活动表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_activity")
public class LotteryActivity extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 活动编码.
*/
@TableField("activity_code")
private String activityCode;
/**
* 活动名称.
*/
@TableField("activity_name")
private String activityName;
/**
* 活动描述.
*/
@TableField("activity_desc")
private String activityDesc;
/**
* 开始时间.
*/
@TableField("start_time")
private LocalDateTime startTime;
/**
* 结束时间.
*/
@TableField("end_time")
private LocalDateTime endTime;
/**
* 状态0-未开始1-进行中2-已结束3-已关闭.
*/
@TableField("status")
private Integer status;
/**
* 总奖品库存.
*/
@TableField("total_stock")
private Integer totalStock;
/**
* 已消耗库存.
*/
@TableField("consumed_stock")
private Integer consumedStock;
/**
* 每人抽奖次数限制0为不限制.
*/
@TableField("draw_count_limit")
private Integer drawCountLimit;
/**
* 次数限制周期DAILY-每天TOTAL-总计.
*/
@TableField("draw_count_cycle")
private String drawCountCycle;
/**
* 是否需要抽奖券0-1-.
*/
@TableField("need_ticket")
private Integer needTicket;
/**
* 每次消耗抽奖券数量.
*/
@TableField("ticket_cost")
private Integer ticketCost;
/**
* 排序.
*/
@TableField("sort_order")
private Integer sortOrder;
}

View File

@ -0,0 +1,114 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 奖品配置表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_prize")
public class LotteryPrize extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 活动ID.
*/
@TableField("activity_id")
private Long activityId;
/**
* 奖品编码.
*/
@TableField("prize_code")
private String prizeCode;
/**
* 奖品名称.
*/
@TableField("prize_name")
private String prizeName;
/**
* 奖品类型1-实物2-虚拟币3-优惠券4-积分5-谢谢参与.
*/
@TableField("prize_type")
private Integer prizeType;
/**
* 奖品价值/数量.
*/
@TableField("prize_value")
private BigDecimal prizeValue;
/**
* 奖品图片.
*/
@TableField("prize_image")
private String prizeImage;
/**
* 奖品等级1-特等奖2-一等奖3-二等奖.
*/
@TableField("prize_level")
private Integer prizeLevel;
/**
* 总库存.
*/
@TableField("total_stock")
private Integer totalStock;
/**
* 剩余库存.
*/
@TableField("remaining_stock")
private Integer remainingStock;
/**
* 每日库存限制0为不限制.
*/
@TableField("daily_stock")
private Integer dailyStock;
/**
* 中奖概率0-1之间.
*/
@TableField("probability")
private BigDecimal probability;
/**
* 排序九宫格位置.
*/
@TableField("sort_order")
private Integer sortOrder;
/**
* 是否展示0-1-.
*/
@TableField("is_display")
private Integer isDisplay;
}

View File

@ -0,0 +1,102 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 中奖记录表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_record")
public class LotteryRecord extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 记录编号.
*/
@TableField("record_no")
private String recordNo;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 活动ID.
*/
@TableField("activity_id")
private Long activityId;
/**
* 奖品ID未中奖为NULL.
*/
@TableField("prize_id")
private Long prizeId;
/**
* 奖品名称快照.
*/
@TableField("prize_name")
private String prizeName;
/**
* 奖品类型快照.
*/
@TableField("prize_type")
private Integer prizeType;
/**
* 是否中奖0-未中奖1-中奖.
*/
@TableField("is_win")
private Integer isWin;
/**
* 抽奖时间.
*/
@TableField("draw_time")
private LocalDateTime drawTime;
/**
* 奖品状态0-待发放1-已发放2-发放失败.
*/
@TableField("prize_status")
private Integer prizeStatus;
/**
* 发放时间.
*/
@TableField("deliver_time")
private LocalDateTime deliverTime;
/**
* 备注.
*/
@TableField("remark")
private String remark;
}

View File

@ -0,0 +1,84 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 抽奖券表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_ticket")
public class LotteryTicket extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 来源SYSTEM-系统发放ACTIVITY-活动赠送PURCHASE-购买.
*/
@TableField("ticket_source")
private String ticketSource;
/**
* 来源ID.
*/
@TableField("source_id")
private String sourceId;
/**
* 总数量.
*/
@TableField("total_count")
private Integer totalCount;
/**
* 已使用数量.
*/
@TableField("used_count")
private Integer usedCount;
/**
* 剩余数量.
*/
@TableField("remaining_count")
private Integer remainingCount;
/**
* 过期时间.
*/
@TableField("expire_time")
private LocalDateTime expireTime;
/**
* 状态0-已失效1-正常.
*/
@TableField("status")
private Integer status;
}

View File

@ -0,0 +1,71 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 抽奖券使用记录表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_ticket_record")
public class LotteryTicketRecord extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 抽奖券ID.
*/
@TableField("ticket_id")
private Long ticketId;
/**
* 变动数量正数增加负数减少.
*/
@TableField("change_count")
private Integer changeCount;
/**
* 变动类型1-发放2-消耗3-过期4-退回.
*/
@TableField("change_type")
private Integer changeType;
/**
* 关联的抽奖记录ID.
*/
@TableField("lottery_record_id")
private Long lotteryRecordId;
/**
* 备注.
*/
@TableField("remark")
private String remark;
}

View File

@ -0,0 +1,60 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.time.LocalDate;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 用户抽奖次数统计表.
* </p>
*
* @author system
* @since 2025-10-18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("lottery_user_count")
public class LotteryUserCount extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId("id")
private Long id;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 活动ID.
*/
@TableField("activity_id")
private Long activityId;
/**
* 统计日期.
*/
@TableField("draw_date")
private LocalDate drawDate;
/**
* 抽奖次数.
*/
@TableField("draw_count")
private Integer drawCount;
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
/**
* 抽奖活动服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryActivityService extends IService<LotteryActivity> {
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
/**
* 奖品配置服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryPrizeService extends IService<LotteryPrize> {
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
/**
* 中奖记录服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryRecordService extends IService<LotteryRecord> {
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
/**
* 抽奖券使用记录服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryTicketRecordService extends IService<LotteryTicketRecord> {
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
/**
* 抽奖券服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryTicketService extends IService<LotteryTicket> {
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
/**
* 用户抽奖次数统计服务.
*
* @author system
* @since 2025-10-18
*/
public interface LotteryUserCountService extends IService<LotteryUserCount> {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryActivityMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import org.springframework.stereotype.Service;
/**
* 抽奖活动服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryActivityServiceImpl extends ServiceImpl<LotteryActivityMapper, LotteryActivity>
implements LotteryActivityService {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryPrizeMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import org.springframework.stereotype.Service;
/**
* 奖品配置服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryPrizeServiceImpl extends ServiceImpl<LotteryPrizeMapper, LotteryPrize>
implements LotteryPrizeService {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryRecordMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
import org.springframework.stereotype.Service;
/**
* 中奖记录服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryRecordServiceImpl extends ServiceImpl<LotteryRecordMapper, LotteryRecord>
implements LotteryRecordService {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryTicketRecordMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
import org.springframework.stereotype.Service;
/**
* 抽奖券使用记录服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryTicketRecordServiceImpl extends ServiceImpl<LotteryTicketRecordMapper, LotteryTicketRecord>
implements LotteryTicketRecordService {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryTicketMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import org.springframework.stereotype.Service;
/**
* 抽奖券服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryTicketServiceImpl extends ServiceImpl<LotteryTicketMapper, LotteryTicket>
implements LotteryTicketService {
}

View File

@ -0,0 +1,19 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryUserCountMapper;
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
import org.springframework.stereotype.Service;
/**
* 用户抽奖次数统计服务实现.
*
* @author system
* @since 2025-10-18
*/
@Service
public class LotteryUserCountServiceImpl extends ServiceImpl<LotteryUserCountMapper, LotteryUserCount>
implements LotteryUserCountService {
}

View File

@ -0,0 +1,75 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityClientApi;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 抽奖活动ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryActivityClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryActivityClientEndpoint implements LotteryActivityClientApi {
private final LotteryActivityService lotteryActivityService;
@Override
public ResultResponse<LotteryActivityDTO> getByActivityCode(String activityCode) {
LotteryActivity activity = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, activityCode)
.one();
if (activity == null) {
return ResultResponse.success(null);
}
return ResultResponse.success(convertToDTO(activity));
}
@Override
public ResultResponse<LotteryActivityDTO> getById(Long id) {
LotteryActivity activity = lotteryActivityService.getById(id);
if (activity == null) {
return ResultResponse.success(null);
}
return ResultResponse.success(convertToDTO(activity));
}
@Override
public ResultResponse<List<LotteryActivityDTO>> listActivities() {
List<LotteryActivity> activities = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getStatus, 1)
.orderByDesc(LotteryActivity::getCreateTime)
.list();
List<LotteryActivityDTO> dtoList = activities.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return ResultResponse.success(dtoList);
}
private LotteryActivityDTO convertToDTO(LotteryActivity activity) {
LotteryActivityDTO dto = new LotteryActivityDTO();
BeanUtils.copyProperties(activity, dto);
return dto;
}
}

View File

@ -0,0 +1,97 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityManageClientApi;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivitySaveCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 抽奖活动管理ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryActivityManageClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryActivityManageClientEndpoint implements LotteryActivityManageClientApi {
private final LotteryActivityService lotteryActivityService;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> createActivity(LotteryActivitySaveCmd cmd) {
// 校验活动编码唯一性
LotteryActivity exists = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, cmd.getActivityCode())
.one();
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_CODE_EXISTS, exists == null);
LotteryActivity activity = new LotteryActivity();
BeanUtils.copyProperties(cmd, activity);
activity.setStatus(0); // 未开始
activity.setTotalStock(0);
activity.setConsumedStock(0);
lotteryActivityService.save(activity);
return ResultResponse.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> updateActivity(LotteryActivitySaveCmd cmd) {
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_ID_REQUIRED, cmd.getId() != null);
LotteryActivity activity = lotteryActivityService.getById(cmd.getId());
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_NOT_FOUND, activity != null);
// 校验活动编码唯一性
LotteryActivity exists = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, cmd.getActivityCode())
.ne(LotteryActivity::getId, cmd.getId())
.one();
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_CODE_EXISTS, exists == null);
BeanUtils.copyProperties(cmd, activity);
lotteryActivityService.updateById(activity);
return ResultResponse.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> updateActivityStatus(Long id, Integer status) {
LotteryActivity activity = lotteryActivityService.getById(id);
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_NOT_FOUND, activity != null);
activity.setStatus(status);
lotteryActivityService.updateById(activity);
return ResultResponse.success();
}
@Override
public ResultResponse<LotteryActivityDTO> getById(Long id) {
LotteryActivity activity = lotteryActivityService.getById(id);
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_NOT_FOUND, activity != null);
LotteryActivityDTO dto = new LotteryActivityDTO();
BeanUtils.copyProperties(activity, dto);
return ResultResponse.success(dto);
}
}

View File

@ -0,0 +1,76 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryActivityQueryClientApi;
import com.red.circle.other.inner.model.cmd.activity.LotteryActivityQryCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 活动查询ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryActivityQueryClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryActivityQueryClientEndpoint implements LotteryActivityQueryClientApi {
private final LotteryActivityService lotteryActivityService;
@Override
public ResultResponse<PageResult<LotteryActivityDTO>> listActivities(LotteryActivityQryCmd cmd) {
Integer pageNo = cmd.getPageNo() != null ? cmd.getPageNo() : 1;
Integer pageSize = cmd.getPageSize() != null ? cmd.getPageSize() : 20;
Page<LotteryActivity> page = new Page<>(pageNo, pageSize);
var query = lotteryActivityService.lambdaQuery();
if (cmd.getActivityName() != null) {
query.like(LotteryActivity::getActivityName, cmd.getActivityName());
}
if (cmd.getActivityCode() != null) {
query.eq(LotteryActivity::getActivityCode, cmd.getActivityCode());
}
if (cmd.getStatus() != null) {
query.eq(LotteryActivity::getStatus, cmd.getStatus());
}
if (cmd.getStartTimeBegin() != null) {
query.ge(LotteryActivity::getStartTime, cmd.getStartTimeBegin());
}
if (cmd.getStartTimeEnd() != null) {
query.le(LotteryActivity::getStartTime, cmd.getStartTimeEnd());
}
Page<LotteryActivity> result = query.orderByDesc(LotteryActivity::getCreateTime).page(page);
List<LotteryActivityDTO> dtoList = result.getRecords().stream()
.map(activity -> {
LotteryActivityDTO dto = new LotteryActivityDTO();
BeanUtils.copyProperties(activity, dto);
return dto;
})
.collect(Collectors.toList());
PageResult<LotteryActivityDTO> pageResult = new PageResult<>();
pageResult.setCurrent(pageNo);
pageResult.setSize(pageSize);
pageResult.setTotal(result.getTotal());
pageResult.setRecords(dtoList);
return ResultResponse.success(pageResult);
}
}

View File

@ -0,0 +1,73 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryPrizeClientApi;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 奖品配置ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryPrizeClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryPrizeClientEndpoint implements LotteryPrizeClientApi {
private final LotteryPrizeService lotteryPrizeService;
@Override
public ResultResponse<List<LotteryPrizeDTO>> listByActivityId(Long activityId) {
List<LotteryPrize> prizes = lotteryPrizeService.lambdaQuery()
.eq(LotteryPrize::getActivityId, activityId)
.orderByAsc(LotteryPrize::getSortOrder)
.list();
List<LotteryPrizeDTO> dtoList = prizes.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return ResultResponse.success(dtoList);
}
@Override
public ResultResponse<LotteryPrizeDTO> getById(Long id) {
LotteryPrize prize = lotteryPrizeService.getById(id);
if (prize == null) {
return ResultResponse.success(null);
}
return ResultResponse.success(convertToDTO(prize));
}
@Override
public ResultResponse<Boolean> deductStock(Long id, Integer count) {
boolean success = lotteryPrizeService.lambdaUpdate()
.eq(LotteryPrize::getId, id)
.gt(LotteryPrize::getRemainingStock, 0)
.setSql("remaining_stock = remaining_stock - " + count)
.update();
return ResultResponse.success(success);
}
private LotteryPrizeDTO convertToDTO(LotteryPrize prize) {
LotteryPrizeDTO dto = new LotteryPrizeDTO();
BeanUtils.copyProperties(prize, dto);
return dto;
}
}

View File

@ -0,0 +1,129 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryPrize;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode;
import com.red.circle.other.inner.endpoint.activity.api.LotteryPrizeManageClientApi;
import com.red.circle.other.inner.model.cmd.activity.LotteryPrizeSaveCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryPrizeDTO;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 奖品管理ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryPrizeManageClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryPrizeManageClientEndpoint implements LotteryPrizeManageClientApi {
private final LotteryPrizeService lotteryPrizeService;
private final LotteryActivityService lotteryActivityService;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> savePrize(LotteryPrizeSaveCmd cmd) {
// 校验活动存在
LotteryActivity activity = lotteryActivityService.getById(cmd.getActivityId());
ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_NOT_FOUND, activity != null);
LotteryPrize prize;
if (cmd.getId() != null) {
// 更新
prize = lotteryPrizeService.getById(cmd.getId());
ResponseAssert.isTrue(LotteryErrorCode.PRIZE_NOT_FOUND, prize != null);
BeanUtils.copyProperties(cmd, prize);
} else {
// 新增
prize = new LotteryPrize();
BeanUtils.copyProperties(cmd, prize);
prize.setRemainingStock(cmd.getTotalStock());
}
lotteryPrizeService.saveOrUpdate(prize);
// 更新活动总库存
updateActivityStockInternal(cmd.getActivityId());
return ResultResponse.success();
}
@Override
public ResultResponse<List<LotteryPrizeDTO>> listByActivityId(Long activityId) {
List<LotteryPrize> prizes = lotteryPrizeService.lambdaQuery()
.eq(LotteryPrize::getActivityId, activityId)
.orderByAsc(LotteryPrize::getSortOrder)
.list();
List<LotteryPrizeDTO> dtoList = prizes.stream()
.map(prize -> {
LotteryPrizeDTO dto = new LotteryPrizeDTO();
BeanUtils.copyProperties(prize, dto);
return dto;
})
.collect(Collectors.toList());
return ResultResponse.success(dtoList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> deletePrize(Long id) {
LotteryPrize prize = lotteryPrizeService.getById(id);
ResponseAssert.isTrue(LotteryErrorCode.PRIZE_NOT_FOUND, prize != null);
Long activityId = prize.getActivityId();
lotteryPrizeService.removeById(id);
// 更新活动总库存
updateActivityStockInternal(activityId);
return ResultResponse.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> updateActivityStock(Long activityId) {
updateActivityStockInternal(activityId);
return ResultResponse.success();
}
/**
* 更新活动总库存内部方法.
*/
private void updateActivityStockInternal(Long activityId) {
List<LotteryPrize> prizes = lotteryPrizeService.lambdaQuery()
.eq(LotteryPrize::getActivityId, activityId)
.list();
int totalStock = prizes.stream()
.mapToInt(LotteryPrize::getTotalStock)
.sum();
int remainingStock = prizes.stream()
.mapToInt(LotteryPrize::getRemainingStock)
.sum();
lotteryActivityService.lambdaUpdate()
.eq(LotteryActivity::getId, activityId)
.set(LotteryActivity::getTotalStock, totalStock)
.set(LotteryActivity::getConsumedStock, totalStock - remainingStock)
.update();
}
}

View File

@ -0,0 +1,66 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryRecordClientApi;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 中奖记录ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryRecordClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryRecordClientEndpoint implements LotteryRecordClientApi {
private final LotteryRecordService lotteryRecordService;
@Override
public ResultResponse<Long> saveRecord(String recordNo, Long userId, Long activityId,
Long prizeId, String prizeName, Integer prizeType,
Integer isWin) {
LotteryRecord record = new LotteryRecord();
record.setRecordNo(recordNo);
record.setUserId(userId);
record.setActivityId(activityId);
record.setPrizeId(prizeId);
record.setPrizeName(prizeName);
record.setPrizeType(prizeType);
record.setIsWin(isWin);
record.setDrawTime(LocalDateTime.now());
record.setPrizeStatus(isWin == 1 ? 0 : null);
lotteryRecordService.save(record);
return ResultResponse.success(record.getId());
}
@Override
public ResultResponse<LotteryRecordDTO> getByRecordNo(String recordNo) {
LotteryRecord record = lotteryRecordService.lambdaQuery()
.eq(LotteryRecord::getRecordNo, recordNo)
.one();
if (record == null) {
return ResultResponse.success(null);
}
LotteryRecordDTO dto = new LotteryRecordDTO();
BeanUtils.copyProperties(record, dto);
return ResultResponse.success(dto);
}
}

View File

@ -0,0 +1,135 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryActivity;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode;
import com.red.circle.other.inner.endpoint.activity.api.LotteryRecordManageClientApi;
import com.red.circle.other.inner.model.cmd.activity.LotteryRecordQryCmd;
import com.red.circle.other.inner.model.dto.activity.LotteryActivityDTO;
import com.red.circle.other.inner.model.dto.activity.LotteryRecordDTO;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 中奖记录管理ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryRecordManageClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryRecordManageClientEndpoint implements LotteryRecordManageClientApi {
private final LotteryRecordService lotteryRecordService;
private final LotteryActivityService lotteryActivityService;
@Override
public ResultResponse<PageResult<LotteryRecordDTO>> listRecords(LotteryRecordQryCmd cmd) {
Integer pageNo = cmd.getPageNo() != null ? cmd.getPageNo() : 1;
Integer pageSize = cmd.getPageSize() != null ? cmd.getPageSize() : 20;
Page<LotteryRecord> page = new Page<>(pageNo, pageSize);
var query = lotteryRecordService.lambdaQuery();
if (cmd.getUserId() != null) {
query.eq(LotteryRecord::getUserId, cmd.getUserId());
}
if (cmd.getActivityId() != null) {
query.eq(LotteryRecord::getActivityId, cmd.getActivityId());
}
if (cmd.getActivityCode() != null) {
LotteryActivity activity = lotteryActivityService.lambdaQuery()
.eq(LotteryActivity::getActivityCode, cmd.getActivityCode())
.one();
if (activity != null) {
query.eq(LotteryRecord::getActivityId, activity.getId());
}
}
if (cmd.getIsWin() != null) {
query.eq(LotteryRecord::getIsWin, cmd.getIsWin());
}
if (cmd.getPrizeStatus() != null) {
query.eq(LotteryRecord::getPrizeStatus, cmd.getPrizeStatus());
}
if (cmd.getDrawTimeBegin() != null) {
query.ge(LotteryRecord::getDrawTime, cmd.getDrawTimeBegin());
}
if (cmd.getDrawTimeEnd() != null) {
query.le(LotteryRecord::getDrawTime, cmd.getDrawTimeEnd());
}
Page<LotteryRecord> result = query.orderByDesc(LotteryRecord::getDrawTime).page(page);
// 查询活动信息
List<Long> activityIds = result.getRecords().stream()
.map(LotteryRecord::getActivityId)
.distinct()
.collect(Collectors.toList());
Map<Long, LotteryActivity> activityMap = lotteryActivityService.lambdaQuery()
.in(LotteryActivity::getId, activityIds)
.list()
.stream()
.collect(Collectors.toMap(LotteryActivity::getId, a -> a));
List<LotteryRecordDTO> dtoList = result.getRecords().stream()
.map(record -> {
LotteryRecordDTO dto = new LotteryRecordDTO();
BeanUtils.copyProperties(record, dto);
LotteryActivity activity = activityMap.get(record.getActivityId());
if (activity != null) {
dto.setActivityName(activity.getActivityName());
}
return dto;
})
.collect(Collectors.toList());
PageResult<LotteryRecordDTO> pageResult = new PageResult<>();
pageResult.setCurrent(pageNo);
pageResult.setSize(pageSize);
pageResult.setTotal(result.getTotal());
pageResult.setRecords(dtoList);
return ResultResponse.success(pageResult);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> deliverPrize(Long recordId) {
LotteryRecord record = lotteryRecordService.getById(recordId);
ResponseAssert.isTrue(LotteryErrorCode.RECORD_NOT_FOUND, record != null);
ResponseAssert.isTrue(LotteryErrorCode.NOT_WIN, record.getIsWin() == 1);
ResponseAssert.isTrue(LotteryErrorCode.PRIZE_ALREADY_DELIVERED, record.getPrizeStatus() == 0);
// TODO: 根据奖品类型执行不同的发放逻辑
// 1-实物生成发货单
// 2-虚拟币调用钱包服务
// 3-优惠券调用优惠券服务
// 4-积分调用积分服务
record.setPrizeStatus(1); // 已发放
record.setDeliverTime(LocalDateTime.now());
lotteryRecordService.updateById(record);
return ResultResponse.success();
}
}

View File

@ -0,0 +1,120 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryTicketClientApi;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 抽奖券ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryTicketClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryTicketClientEndpoint implements LotteryTicketClientApi {
private final LotteryTicketService lotteryTicketService;
private final LotteryTicketRecordService lotteryTicketRecordService;
@Override
public ResultResponse<Integer> getUserTicketCount(Long userId) {
List<LotteryTicket> tickets = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.list();
int totalCount = tickets.stream()
.mapToInt(LotteryTicket::getRemainingCount)
.sum();
return ResultResponse.success(totalCount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Boolean> deductTicket(Long userId, Integer count) {
List<LotteryTicket> tickets = lotteryTicketService.lambdaQuery()
.eq(LotteryTicket::getUserId, userId)
.eq(LotteryTicket::getStatus, 1)
.gt(LotteryTicket::getRemainingCount, 0)
.orderByAsc(LotteryTicket::getExpireTime)
.list();
int totalRemaining = tickets.stream()
.mapToInt(LotteryTicket::getRemainingCount)
.sum();
if (totalRemaining < count) {
return ResultResponse.success(false);
}
int remaining = count;
for (LotteryTicket ticket : tickets) {
if (remaining <= 0) {
break;
}
int deduct = Math.min(remaining, ticket.getRemainingCount());
lotteryTicketService.lambdaUpdate()
.eq(LotteryTicket::getId, ticket.getId())
.setSql("used_count = used_count + " + deduct)
.setSql("remaining_count = remaining_count - " + deduct)
.update();
// 记录使用
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
ticketRecord.setUserId(userId);
ticketRecord.setTicketId(ticket.getId());
ticketRecord.setChangeCount(-deduct);
ticketRecord.setChangeType(2); // 消耗
ticketRecord.setRemark("抽奖消耗");
lotteryTicketRecordService.save(ticketRecord);
remaining -= deduct;
}
return ResultResponse.success(true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Boolean> addTicket(Long userId, Integer count, String source, String sourceId) {
LotteryTicket ticket = new LotteryTicket();
ticket.setUserId(userId);
ticket.setTicketSource(source != null ? source : "SYSTEM");
ticket.setSourceId(sourceId);
ticket.setTotalCount(count);
ticket.setUsedCount(0);
ticket.setRemainingCount(count);
ticket.setStatus(1);
lotteryTicketService.save(ticket);
// 记录发放
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
ticketRecord.setUserId(userId);
ticketRecord.setTicketId(ticket.getId());
ticketRecord.setChangeCount(count);
ticketRecord.setChangeType(1); // 发放
ticketRecord.setRemark("系统发放");
lotteryTicketRecordService.save(ticketRecord);
return ResultResponse.success(true);
}
}

View File

@ -0,0 +1,62 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryTicketManageClientApi;
import com.red.circle.other.inner.model.cmd.activity.LotteryTicketGrantCmd;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 抽奖券管理ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryTicketManageClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryTicketManageClientEndpoint implements LotteryTicketManageClientApi {
private final LotteryTicketService lotteryTicketService;
private final LotteryTicketRecordService lotteryTicketRecordService;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultResponse<Void> grantTickets(LotteryTicketGrantCmd cmd) {
String ticketSource = cmd.getTicketSource() != null ? cmd.getTicketSource() : "SYSTEM";
for (Long userId : cmd.getUserIds()) {
LotteryTicket ticket = new LotteryTicket();
ticket.setUserId(userId);
ticket.setTicketSource(ticketSource);
ticket.setSourceId(cmd.getSourceId());
ticket.setTotalCount(cmd.getCount());
ticket.setUsedCount(0);
ticket.setRemainingCount(cmd.getCount());
ticket.setExpireTime(cmd.getExpireTime());
ticket.setStatus(1);
lotteryTicketService.save(ticket);
// 记录发放
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
ticketRecord.setUserId(userId);
ticketRecord.setTicketId(ticket.getId());
ticketRecord.setChangeCount(cmd.getCount());
ticketRecord.setChangeType(1); // 发放
ticketRecord.setRemark(cmd.getRemark());
lotteryTicketRecordService.save(ticketRecord);
}
return ResultResponse.success();
}
}

View File

@ -0,0 +1,66 @@
package com.red.circle.other.app.inner.endpoint.activity;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
import com.red.circle.other.inner.endpoint.activity.api.LotteryUserCountClientApi;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户抽奖次数统计ClientEndpoint.
*
* @author system
* @since 2025-10-18
*/
@Validated
@RestController
@RequestMapping(value = LotteryUserCountClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class LotteryUserCountClientEndpoint implements LotteryUserCountClientApi {
private final LotteryUserCountService lotteryUserCountService;
@Override
public ResultResponse<Integer> getTodayCount(Long userId, Long activityId) {
LotteryUserCount userCount = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.eq(LotteryUserCount::getActivityId, activityId)
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.one();
int count = userCount != null ? userCount.getDrawCount() : 0;
return ResultResponse.success(count);
}
@Override
public ResultResponse<Void> incrDrawCount(Long userId, Long activityId) {
LotteryUserCount userCount = lotteryUserCountService.lambdaQuery()
.eq(LotteryUserCount::getUserId, userId)
.eq(LotteryUserCount::getActivityId, activityId)
.eq(LotteryUserCount::getDrawDate, LocalDate.now())
.one();
if (userCount == null) {
userCount = new LotteryUserCount();
userCount.setUserId(userId);
userCount.setActivityId(activityId);
userCount.setDrawDate(LocalDate.now());
userCount.setDrawCount(1);
lotteryUserCountService.save(userCount);
} else {
lotteryUserCountService.lambdaUpdate()
.eq(LotteryUserCount::getId, userCount.getId())
.setSql("draw_count = draw_count + 1")
.update();
}
return ResultResponse.success();
}
}