Browse Source

对接拼团数据1

master
wangfukang 2 weeks ago
parent
commit
a996f3b886
  1. 16
      hiver-admin/test-output/test-report.html
  2. 7
      hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java
  3. 96
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/constant/PlanetConstant.java
  4. 235
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java
  5. 234
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetController.java
  6. 67
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetBuff.java
  7. 68
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetBuffRecord.java
  8. 66
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetDrawRecord.java
  9. 78
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetDrawWinner.java
  10. 66
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetHuntRecord.java
  11. 57
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetNews.java
  12. 74
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetPool.java
  13. 64
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetReward.java
  14. 63
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTask.java
  15. 91
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTicket.java
  16. 60
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTicketLog.java
  17. 56
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/hook/PlanetRewardHook.java
  18. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetBuffMapper.java
  19. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetBuffRecordMapper.java
  20. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetDrawRecordMapper.java
  21. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetDrawWinnerMapper.java
  22. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetHuntRecordMapper.java
  23. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetNewsMapper.java
  24. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetPoolMapper.java
  25. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetRewardMapper.java
  26. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTaskMapper.java
  27. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTicketLogMapper.java
  28. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTicketMapper.java
  29. 54
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetMqConfig.java
  30. 109
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetTicketConsumer.java
  31. 60
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetTicketProducer.java
  32. 34
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetBoxResultVo.java
  33. 51
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetBuffVo.java
  34. 83
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetHomeVo.java
  35. 40
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetHuntResultVo.java
  36. 52
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetQuery.java
  37. 55
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetRankItemVo.java
  38. 46
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetTaskVo.java
  39. 65
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetAdminService.java
  40. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetBoxService.java
  41. 63
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetBuffService.java
  42. 52
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetDrawService.java
  43. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetHuntService.java
  44. 23
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetNewsService.java
  45. 29
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetRankService.java
  46. 17
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetService.java
  47. 41
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetTaskService.java
  48. 50
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetTicketService.java
  49. 288
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetAdminServiceImpl.java
  50. 107
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetBoxServiceImpl.java
  51. 276
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetBuffServiceImpl.java
  52. 393
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java
  53. 198
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetHuntServiceImpl.java
  54. 54
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetNewsServiceImpl.java
  55. 157
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetRankServiceImpl.java
  56. 104
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java
  57. 199
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetTaskServiceImpl.java
  58. 229
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetTicketServiceImpl.java
  59. 86
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/PlanetDailyResetTask.java
  60. 33
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/PlanetDrawTask.java
  61. 5
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java
  62. 2
      hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_persona_images.sql
  63. 244
      hiver-modules/hiver-mall/src/main/resources/sql/planet.sql

16
hiver-admin/test-output/test-report.html

@ -35,7 +35,7 @@
<a href="#"><span class="badge badge-primary">Hiver</span></a>
</li>
<li class="m-r-10">
<a href="#"><span class="badge badge-primary">五月 29, 2026 16:35:42</span></a>
<a href="#"><span class="badge badge-primary">五月 30, 2026 13:54:26</span></a>
</li>
</ul>
</div>
@ -84,7 +84,7 @@
<div class="test-detail">
<span class="meta text-white badge badge-sm"></span>
<p class="name">passTest</p>
<p class="text-sm"><span>16:35:42 下午</span> / <span>0.018 secs</span></p>
<p class="text-sm"><span>13:54:27 下午</span> / <span>0.034 secs</span></p>
</div>
<div class="test-contents d-none">
<div class="detail-head">
@ -92,9 +92,9 @@
<div class="info">
<div class='float-right'><span class='badge badge-default'>#test-id=1</span></div>
<h5 class="test-status text-pass">passTest</h5>
<span class='badge badge-success'>05.29.2026 16:35:42</span>
<span class='badge badge-danger'>05.29.2026 16:35:42</span>
<span class='badge badge-default'>0.018 secs</span>
<span class='badge badge-success'>05.30.2026 13:54:27</span>
<span class='badge badge-danger'>05.30.2026 13:54:27</span>
<span class='badge badge-default'>0.034 secs</span>
</div>
<div class="m-t-10 m-l-5"></div>
</div>
@ -104,7 +104,7 @@
<tbody>
<tr class="event-row">
<td><span class="badge log pass-bg">Pass</span></td>
<td>16:35:42</td>
<td>13:54:27</td>
<td>
Test passed
</td>
@ -128,13 +128,13 @@
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Started</p>
<h3>五月 29, 2026 16:35:42</h3>
<h3>五月 30, 2026 13:54:26</h3>
</div></div>
</div>
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Ended</p>
<h3>五月 29, 2026 16:35:43</h3>
<h3>五月 30, 2026 13:54:27</h3>
</div></div>
</div>
<div class="col-md-3">

7
hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java

@ -106,6 +106,9 @@ public class UserController {
@Autowired
private SecurityUtil securityUtil;
@Autowired
private cc.hiver.mall.planet.hook.PlanetRewardHook planetRewardHook;
@PersistenceContext
private EntityManager entityManager;
@ -656,6 +659,10 @@ public class UserController {
userRoleService.save(ur);
}
}
// 白嫖星球-邀请好友注册成功,给邀请人发放星球券(不影响主流程)
if (wechatVo.getInviterId() != null) {
planetRewardHook.onInviteRegister(wechatVo.getInviterId(), null, wechatUser.getId());
}
// 更新客户信息,绑定登录用户
//customerService.updateUserId(wechatVo.getCustomId(), wechatUser.getId());
// 返回当前登录人的信息

96
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/constant/PlanetConstant.java

@ -0,0 +1,96 @@
package cc.hiver.mall.planet.constant;
/**
* 白嫖星球常量
*
* @author hiver
*/
public class PlanetConstant {
/** 奖池状态 0进行中 1已开奖 */
public static final int POOL_STATUS_RUNNING = 0;
public static final int POOL_STATUS_DRAWN = 1;
/** 券流水来源类型 */
public static final String TICKET_TYPE_ORDER = "order";
public static final String TICKET_TYPE_GROUP = "group";
public static final String TICKET_TYPE_INVITE = "invite";
public static final String TICKET_TYPE_SIGN = "sign";
public static final String TICKET_TYPE_BOX = "box";
public static final String TICKET_TYPE_HUNT = "hunt";
public static final String TICKET_TYPE_BUFF = "buff";
public static final String TICKET_TYPE_DRAW = "draw";
/** 任务编码 */
public static final String TASK_WAIMAI = "waimai";
public static final String TASK_GROUP = "group";
public static final String TASK_INVITE = "invite";
public static final String TASK_SIGN = "sign";
/** BUFF类型 */
public static final String BUFF_DOUBLE = "double";
public static final String BUFF_SHIELD = "shield";
public static final String BUFF_LUCKY = "lucky";
public static final String BUFF_HUNT = "hunt";
public static final String BUFF_STEALTH = "stealth";
/** BUFF状态 */
public static final int BUFF_STATUS_ACTIVE = 0;
public static final int BUFF_STATUS_EXPIRED = 1;
/** 追捕结果 */
public static final String HUNT_SUCCESS = "success";
public static final String HUNT_SHIELD = "shield";
public static final String HUNT_FAIL = "fail";
/** 每日追捕次数上限 */
public static final int HUNT_DAILY_LIMIT = 3;
/** 每日被追捕成功上限(达到自动开防护罩) */
public static final int HUNTED_SUCCESS_LIMIT = 3;
/** 自动防护罩持续小时 */
public static final int AUTO_SHIELD_HOURS = 24;
/** 危险等级 */
public static String dangerLevel(int ticket) {
if (ticket > 30) {
return "SSS";
} else if (ticket >= 21) {
return "S";
} else if (ticket >= 11) {
return "A";
} else if (ticket >= 4) {
return "B";
}
return "C";
}
public static String dangerLevelName(int ticket) {
if (ticket > 30) {
return "宇宙首富";
} else if (ticket >= 21) {
return "S级目标";
} else if (ticket >= 11) {
return "A级目标";
} else if (ticket >= 4) {
return "B级目标";
}
return "C级目标";
}
/** 榜单目标悬赏奖励:按排名前三额外悬赏券 */
public static int bountyByRank(int rankNo) {
if (rankNo == 1) {
return 2;
} else if (rankNo == 2 || rankNo == 3) {
return 1;
}
return 0;
}
/** 快讯类型 */
public static final String NEWS_RANK = "rank";
public static final String NEWS_HUNT = "hunt";
public static final String NEWS_BOX = "box";
public static final String NEWS_DRAW = "draw";
public static final String NEWS_SYS = "sys";
}

235
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java

@ -0,0 +1,235 @@
package cc.hiver.mall.planet.controller;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.core.common.utils.ResultUtil;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.core.common.vo.Result;
import cc.hiver.mall.planet.entity.*;
import cc.hiver.mall.planet.service.PlanetAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
/**
* 白嫖星球-后台管理接口
*
* @author hiver
*/
@Slf4j
@RestController
@Api(tags = "白嫖星球后台接口")
@RequestMapping("/hiver/app/planet/admin")
@Transactional
public class PlanetAdminController {
@Autowired
private PlanetAdminService adminService;
/** 商圈强隔离:白嫖星球后台所有功能均限定在本商圈内 */
private static final String REGION_REQUIRED = "请先选择商圈,白嫖星球仅限本区域内进行";
private PageVo buildPage(Integer pageNumber, Integer pageSize) {
final PageVo pageVo = new PageVo();
pageVo.setPageNumber(pageNumber == null ? 1 : pageNumber);
pageVo.setPageSize(pageSize == null ? 10 : pageSize);
return pageVo;
}
// ---------------- 奖池 ----------------
@ApiOperation(value = "奖池分页")
@RequestMapping(value = "/pool/page", method = RequestMethod.GET)
public Result poolPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId,
@RequestParam(required = false) Integer status) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error("请先选择商圈,抽奖仅限本区域内进行");
}
return new ResultUtil<>().setData(adminService.pagePools(buildPage(pageNumber, pageSize), regionId, status));
}
@ApiOperation(value = "保存奖池")
@RequestMapping(value = "/pool/save", method = RequestMethod.POST)
public Result poolSave(@RequestBody PlanetPool pool) {
if (pool == null || StringUtils.isEmpty(pool.getRegionId())) {
return ResultUtil.error("奖池必须归属商圈,抽奖仅限本区域内进行");
}
return new ResultUtil<>().setData(adminService.savePool(pool));
}
@ApiOperation(value = "删除奖池")
@RequestMapping(value = "/pool/delete", method = RequestMethod.POST)
public Result poolDelete(@RequestParam String id) {
adminService.deletePool(id);
return ResultUtil.success("删除成功");
}
// ---------------- 奖项 ----------------
@ApiOperation(value = "奖池奖项列表")
@RequestMapping(value = "/reward/list", method = RequestMethod.GET)
public Result rewardList(@RequestParam String poolId) {
return new ResultUtil<>().setData(adminService.rewardsOfPool(poolId));
}
@ApiOperation(value = "保存奖项")
@RequestMapping(value = "/reward/save", method = RequestMethod.POST)
public Result rewardSave(@RequestBody PlanetReward reward) {
return new ResultUtil<>().setData(adminService.saveReward(reward));
}
@ApiOperation(value = "删除奖项")
@RequestMapping(value = "/reward/delete", method = RequestMethod.POST)
public Result rewardDelete(@RequestParam String id) {
adminService.deleteReward(id);
return ResultUtil.success("删除成功");
}
// ---------------- 任务 ----------------
@ApiOperation(value = "任务分页")
@RequestMapping(value = "/task/page", method = RequestMethod.GET)
public Result taskPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(adminService.pageTasks(buildPage(pageNumber, pageSize), regionId));
}
@ApiOperation(value = "保存任务")
@RequestMapping(value = "/task/save", method = RequestMethod.POST)
public Result taskSave(@RequestBody PlanetTask task) {
if (task == null || StringUtils.isEmpty(task.getRegionId())) {
return ResultUtil.error("任务必须归属商圈,白嫖星球仅限本区域内进行");
}
return new ResultUtil<>().setData(adminService.saveTask(task));
}
@ApiOperation(value = "删除任务")
@RequestMapping(value = "/task/delete", method = RequestMethod.POST)
public Result taskDelete(@RequestParam String id) {
adminService.deleteTask(id);
return ResultUtil.success("删除成功");
}
// ---------------- BUFF ----------------
@ApiOperation(value = "BUFF分页")
@RequestMapping(value = "/buff/page", method = RequestMethod.GET)
public Result buffPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(adminService.pageBuffs(buildPage(pageNumber, pageSize), regionId));
}
@ApiOperation(value = "保存BUFF")
@RequestMapping(value = "/buff/save", method = RequestMethod.POST)
public Result buffSave(@RequestBody PlanetBuff buff) {
if (buff == null || StringUtils.isEmpty(buff.getRegionId())) {
return ResultUtil.error("BUFF必须归属商圈,白嫖星球仅限本区域内进行");
}
return new ResultUtil<>().setData(adminService.saveBuff(buff));
}
@ApiOperation(value = "删除BUFF")
@RequestMapping(value = "/buff/delete", method = RequestMethod.POST)
public Result buffDelete(@RequestParam String id) {
adminService.deleteBuff(id);
return ResultUtil.success("删除成功");
}
// ---------------- 快讯 ----------------
@ApiOperation(value = "快讯分页")
@RequestMapping(value = "/news/page", method = RequestMethod.GET)
public Result newsPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(adminService.pageNews(buildPage(pageNumber, pageSize), regionId));
}
@ApiOperation(value = "保存快讯")
@RequestMapping(value = "/news/save", method = RequestMethod.POST)
public Result newsSave(@RequestBody PlanetNews news) {
if (news == null || StringUtils.isEmpty(news.getRegionId())) {
return ResultUtil.error("快讯必须归属商圈,白嫖星球仅限本区域内进行");
}
return new ResultUtil<>().setData(adminService.saveNews(news));
}
@ApiOperation(value = "删除快讯")
@RequestMapping(value = "/news/delete", method = RequestMethod.POST)
public Result newsDelete(@RequestParam String id) {
adminService.deleteNews(id);
return ResultUtil.success("删除成功");
}
// ---------------- 排行榜 ----------------
@ApiOperation(value = "排行榜分页")
@RequestMapping(value = "/rank/page", method = RequestMethod.GET)
public Result rankPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(adminService.pageRank(buildPage(pageNumber, pageSize), regionId));
}
// ---------------- 追捕记录 ----------------
@ApiOperation(value = "追捕记录分页")
@RequestMapping(value = "/hunt/page", method = RequestMethod.GET)
public Result huntPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(adminService.pageHunts(buildPage(pageNumber, pageSize), regionId));
}
// ---------------- 开奖记录 ----------------
@ApiOperation(value = "开奖记录分页")
@RequestMapping(value = "/draw/page", method = RequestMethod.GET)
public Result drawPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error("请先选择商圈,仅可查看本区域开奖记录");
}
return new ResultUtil<>().setData(adminService.pageDraws(buildPage(pageNumber, pageSize), regionId));
}
// ---------------- 中奖记录 ----------------
@ApiOperation(value = "中奖记录分页")
@RequestMapping(value = "/winner/page", method = RequestMethod.GET)
public Result winnerPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String regionId,
@RequestParam(required = false) String periodNo) {
if (StringUtils.isEmpty(regionId)) {
return ResultUtil.error("请先选择商圈,仅可查看本区域中奖记录");
}
return new ResultUtil<>().setData(adminService.pageWinners(buildPage(pageNumber, pageSize), regionId, periodNo));
}
// ---------------- 手动开奖 ----------------
@ApiOperation(value = "手动开奖")
@RequestMapping(value = "/draw/manual", method = RequestMethod.POST)
public Result manualDraw(@RequestParam String poolId,
@RequestParam(required = false) String regionId) {
try {
return new ResultUtil<>().setData(adminService.manualDraw(poolId, regionId));
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
}

234
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetController.java

@ -0,0 +1,234 @@
package cc.hiver.mall.planet.controller;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.core.common.utils.ResultUtil;
import cc.hiver.core.common.vo.Result;
import cc.hiver.mall.planet.entity.PlanetDrawRecord;
import cc.hiver.mall.planet.pojo.PlanetBoxResultVo;
import cc.hiver.mall.planet.pojo.PlanetHomeVo;
import cc.hiver.mall.planet.pojo.PlanetHuntResultVo;
import cc.hiver.mall.planet.pojo.PlanetQuery;
import cc.hiver.mall.planet.service.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 白嫖星球-App端接口
*
* @author hiver
*/
@Slf4j
@RestController
@Api(tags = "白嫖星球App接口")
@RequestMapping("/hiver/app/planet")
@Transactional
public class PlanetController {
/** 商圈强隔离:白嫖星球所有功能均限定在本商圈内 */
private static final String REGION_REQUIRED = "缺少商圈参数,白嫖星球仅限本区域内进行";
@Autowired
private PlanetService planetService;
@Autowired
private PlanetRankService rankService;
@Autowired
private PlanetHuntService huntService;
@Autowired
private PlanetBoxService boxService;
@Autowired
private PlanetTaskService taskService;
@Autowired
private PlanetBuffService buffService;
@Autowired
private PlanetDrawService drawService;
@Autowired
private cc.hiver.mall.planet.service.PlanetTicketService ticketService;
@ApiOperation(value = "首页聚合数据")
@RequestMapping(value = "/home", method = RequestMethod.POST)
public Result home(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getUserId())) {
return ResultUtil.error("用户id不能为空");
}
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error("缺少商圈参数,白嫖星球仅限本区域内进行");
}
try {
final PlanetHomeVo vo = planetService.home(query);
return new ResultUtil<PlanetHomeVo>().setData(vo);
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "通缉榜TOP10")
@RequestMapping(value = "/rank", method = RequestMethod.POST)
public Result rank(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(rankService.rankTop(query.getRegionId(), 10, query.getUserId()));
}
@ApiOperation(value = "发起追捕")
@RequestMapping(value = "/hunt", method = RequestMethod.POST)
public Result hunt(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
try {
final PlanetHuntResultVo vo = huntService.hunt(query.getUserId(), query.getRegionId(), query.getToUserId());
return new ResultUtil<PlanetHuntResultVo>().setData(vo);
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "开启每日宝箱")
@RequestMapping(value = "/box/open", method = RequestMethod.POST)
public Result openBox(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
try {
final PlanetBoxResultVo vo = boxService.openDailyBox(query.getUserId(), query.getRegionId());
return new ResultUtil<PlanetBoxResultVo>().setData(vo);
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "任务列表")
@RequestMapping(value = "/tasks", method = RequestMethod.POST)
public Result tasks(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(taskService.listTasks(query.getUserId(), query.getRegionId()));
}
@ApiOperation(value = "每日签到")
@RequestMapping(value = "/sign", method = RequestMethod.POST)
public Result sign(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
try {
final int reward = taskService.sign(query.getUserId(), query.getRegionId());
return ResultUtil.success("签到成功,获得 " + reward + " 张星球券");
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "领取任务奖励")
@RequestMapping(value = "/task/claim", method = RequestMethod.POST)
public Result claim(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
try {
final int reward = taskService.claim(query.getUserId(), query.getRegionId(), query.getTaskCode());
return ResultUtil.success("领取成功,获得 " + reward + " 张星球券");
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "BUFF商店列表")
@RequestMapping(value = "/buff/list", method = RequestMethod.POST)
public Result buffList(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(buffService.shopList(query.getUserId(), query.getRegionId()));
}
@ApiOperation(value = "购买BUFF")
@RequestMapping(value = "/buff/buy", method = RequestMethod.POST)
public Result buyBuff(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
try {
return new ResultUtil<>().setData(buffService.buyBuff(query.getUserId(), query.getRegionId(), query.getBuffId()));
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "最近一期开奖结果")
@RequestMapping(value = "/draw/result", method = RequestMethod.POST)
public Result drawResult(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error("缺少商圈参数,开奖仅限本区域内进行");
}
try {
final PlanetDrawRecord record = drawService.latestDrawRecord(query.getRegionId());
final Map<String, Object> data = new HashMap<>(4);
data.put("record", record);
data.put("winners", record == null ? null : drawService.winnersOfDraw(record.getId()));
return new ResultUtil<>().setData(data);
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "我的中奖记录")
@RequestMapping(value = "/draw/myWinning", method = RequestMethod.POST)
public Result myWinning(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error("缺少商圈参数,仅可查看本区域中奖记录");
}
try {
return new ResultUtil<>().setData(drawService.myWinning(query.getUserId(), query.getRegionId()));
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "领取中奖奖励")
@RequestMapping(value = "/draw/receive", method = RequestMethod.POST)
public Result receive(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error("缺少商圈参数,仅可领取本区域中奖");
}
try {
drawService.receive(query.getUserId(), query.getRegionId(), query.getWinnerId());
return ResultUtil.success("领取成功");
} catch (HiverException e) {
return ResultUtil.error(e.getMessage());
}
}
@ApiOperation(value = "我的星球券明细(分页)")
@RequestMapping(value = "/ticket/log", method = RequestMethod.POST)
public Result ticketLog(@RequestBody PlanetQuery query) {
if (StringUtils.isEmpty(query.getUserId())) {
return ResultUtil.error("用户id不能为空");
}
if (StringUtils.isEmpty(query.getRegionId())) {
return ResultUtil.error(REGION_REQUIRED);
}
return new ResultUtil<>().setData(
ticketService.pageLog(query.getUserId(), query.getRegionId(), query.getPageNumber(), query.getPageSize()));
}
}

67
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetBuff.java

@ -0,0 +1,67 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-BUFF配置
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-BUFF配置")
@TableName(value = "t_planet_buff", autoResultMap = true)
public class PlanetBuff implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "商圈id 空为全局默认")
private String regionId;
@ApiModelProperty(value = "类型 double/shield/lucky/hunt/stealth")
private String type;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "描述")
private String description;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "消耗券数量")
private Integer costTickets;
@ApiModelProperty(value = "持续小时")
private Integer durationHours;
@ApiModelProperty(value = "效果值")
private BigDecimal effectValue;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "是否启用 1是 0否")
private Integer enabled;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

68
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetBuffRecord.java

@ -0,0 +1,68 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-用户BUFF记录
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-用户BUFF记录")
@TableName(value = "t_planet_buff_record", autoResultMap = true)
public class PlanetBuffRecord implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "用户id")
private String userId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "BUFF配置id")
private String buffId;
@ApiModelProperty(value = "类型")
private String type;
@ApiModelProperty(value = "效果值")
private BigDecimal effectValue;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "生效时间")
private Date startTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "失效时间")
private Date endTime;
@ApiModelProperty(value = "状态 0生效 1已过期")
private Integer status;
@ApiModelProperty(value = "来源 buy购买 box宝箱 hunt防护")
private String source;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

66
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetDrawRecord.java

@ -0,0 +1,66 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-开奖记录
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-开奖记录")
@TableName(value = "t_planet_draw_record", autoResultMap = true)
public class PlanetDrawRecord implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "奖池id")
private String poolId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "期号")
private String periodNo;
@ApiModelProperty(value = "奖池金额")
private BigDecimal poolAmount;
@ApiModelProperty(value = "参与人数")
private Integer joinCount;
@ApiModelProperty(value = "券总数")
private Integer ticketTotal;
@ApiModelProperty(value = "中奖人数")
private Integer winnerCount;
@ApiModelProperty(value = "状态 1已开奖")
private Integer status;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "开奖时间")
private Date drawTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

78
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetDrawWinner.java

@ -0,0 +1,78 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-中奖记录
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-中奖记录")
@TableName(value = "t_planet_draw_winner", autoResultMap = true)
public class PlanetDrawWinner implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "开奖记录id")
private String drawId;
@ApiModelProperty(value = "奖池id")
private String poolId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "期号")
private String periodNo;
@ApiModelProperty(value = "中奖用户id")
private String userId;
@ApiModelProperty(value = "中奖用户昵称")
private String userName;
@ApiModelProperty(value = "奖项层级")
private Integer rewardLevel;
@ApiModelProperty(value = "奖项名称")
private String levelName;
@ApiModelProperty(value = "奖励类型 0现金 1优惠券")
private Integer rewardType;
@ApiModelProperty(value = "中奖金额")
private BigDecimal amount;
@ApiModelProperty(value = "优惠券id")
private String couponId;
@ApiModelProperty(value = "是否已领取 0否 1是")
private Integer isReceived;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "领取时间")
private Date receivedTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

66
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetHuntRecord.java

@ -0,0 +1,66 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 白嫖星球-追捕记录
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-追捕记录")
@TableName(value = "t_planet_hunt_record", autoResultMap = true)
public class PlanetHuntRecord implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "发起人id")
private String fromUserId;
@ApiModelProperty(value = "发起人昵称")
private String fromUserName;
@ApiModelProperty(value = "目标用户id")
private String toUserId;
@ApiModelProperty(value = "目标用户昵称")
private String toUserName;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "结果 success/shield/fail")
private String result;
@ApiModelProperty(value = "缴获券")
private Integer gainTickets;
@ApiModelProperty(value = "悬赏额外券")
private Integer bountyTickets;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "追捕日期")
private LocalDate huntDate;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

57
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetNews.java

@ -0,0 +1,57 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;
/**
* 白嫖星球-星球快讯
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-星球快讯")
@TableName(value = "t_planet_news", autoResultMap = true)
public class PlanetNews implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "类型 rank/hunt/box/draw/sys")
private String type;
@ApiModelProperty(value = "快讯内容")
private String content;
@ApiModelProperty(value = "关联用户id")
private String userId;
@ApiModelProperty(value = "关联用户昵称")
private String userName;
@ApiModelProperty(value = "是否置顶 0否 1是")
private Integer isTop;
@ApiModelProperty(value = "是否展示 1是 0否")
private Integer enabled;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

74
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetPool.java

@ -0,0 +1,74 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-奖池期次
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-奖池期次")
@TableName(value = "t_planet_pool", autoResultMap = true)
public class PlanetPool implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "商圈/校区id")
private String regionId;
@ApiModelProperty(value = "期号")
private String periodNo;
@ApiModelProperty(value = "本期标题")
private String title;
@ApiModelProperty(value = "奖池总金额")
private BigDecimal poolAmount;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "开奖时间")
private Date drawTime;
@ApiModelProperty(value = "参与人数")
private Integer joinCount;
@ApiModelProperty(value = "本期投入券总数")
private Integer ticketTotal;
@ApiModelProperty(value = "中奖人数")
private Integer winnerCount;
@ApiModelProperty(value = "状态 0进行中 1已开奖")
private Integer status;
@ApiModelProperty(value = "备注/公告")
private String remark;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private Date updateTime;
}

64
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetReward.java

@ -0,0 +1,64 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-奖项配置
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-奖项配置")
@TableName(value = "t_planet_reward", autoResultMap = true)
public class PlanetReward implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "奖池id")
private String poolId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "奖项层级 1一等 2二等 3三等 4幸运")
private Integer level;
@ApiModelProperty(value = "奖项名称")
private String levelName;
@ApiModelProperty(value = "奖励类型 0现金 1优惠券")
private Integer rewardType;
@ApiModelProperty(value = "单份金额")
private BigDecimal amount;
@ApiModelProperty(value = "优惠券id")
private String couponId;
@ApiModelProperty(value = "名额数量")
private Integer quota;
@ApiModelProperty(value = "排序")
private Integer sort;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

63
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTask.java

@ -0,0 +1,63 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;
/**
* 白嫖星球-任务规则
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-任务规则")
@TableName(value = "t_planet_task", autoResultMap = true)
public class PlanetTask implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "商圈id 空为全局默认")
private String regionId;
@ApiModelProperty(value = "任务编码 waimai/group/invite/sign")
private String code;
@ApiModelProperty(value = "任务名称")
private String name;
@ApiModelProperty(value = "描述")
private String description;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "奖励券数量")
private Integer rewardTickets;
@ApiModelProperty(value = "每日上限 0不限")
private Integer dailyLimit;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "是否启用 1是 0否")
private Integer enabled;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

91
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTicket.java

@ -0,0 +1,91 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 白嫖星球-用户券汇总
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-用户券汇总")
@TableName(value = "t_planet_ticket", autoResultMap = true)
public class PlanetTicket implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "用户id")
private String userId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "学院")
private String college;
@ApiModelProperty(value = "当前可用券数量")
private Integer ticketCount;
@ApiModelProperty(value = "历史累计获得券")
private Integer totalTicket;
@ApiModelProperty(value = "连续签到天数")
private Integer consecutiveSignDays;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "最近签到日期")
private LocalDate lastSignDate;
@ApiModelProperty(value = "连续霸榜天数")
private Integer rankKeepDays;
@ApiModelProperty(value = "上次排名")
private Integer lastRankNo;
@ApiModelProperty(value = "今日已追捕次数")
private Integer huntCountToday;
@ApiModelProperty(value = "今日被追捕成功次数")
private Integer huntedSuccessToday;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "最近开宝箱日期")
private LocalDate boxOpenedDate;
@ApiModelProperty(value = "所属期号")
private String periodNo;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private Date updateTime;
}

60
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/entity/PlanetTicketLog.java

@ -0,0 +1,60 @@
package cc.hiver.mall.planet.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;
/**
* 白嫖星球-券流水
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-券流水")
@TableName(value = "t_planet_ticket_log", autoResultMap = true)
public class PlanetTicketLog implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "用户id")
private String userId;
@ApiModelProperty(value = "商圈id")
private String regionId;
@ApiModelProperty(value = "变更数量(正得负扣)")
private Integer changeCount;
@ApiModelProperty(value = "变更后余额")
private Integer balance;
@ApiModelProperty(value = "来源 order/group/invite/sign/box/hunt/buff/draw")
private String type;
@ApiModelProperty(value = "来源业务id(幂等)")
private String sourceId;
@ApiModelProperty(value = "期号")
private String periodNo;
@ApiModelProperty(value = "说明")
private String remark;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

56
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/hook/PlanetRewardHook.java

@ -0,0 +1,56 @@
package cc.hiver.mall.planet.hook;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.mq.PlanetTicketProducer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 白嫖星球-业务发券回调
* <p>
* 仅做轻量 MQ 投递所有 DB 操作放到消费端异步执行保证不影响主业务速度和流程
* 任意异常都被吞掉绝不向上抛
*
* @author hiver
*/
@Slf4j
@Component
public class PlanetRewardHook {
@Autowired
private PlanetTicketProducer ticketProducer;
/**
* 外卖/普通订单完成发券
*/
public void onOrderFinish(String userId, String regionId, String orderId) {
rewardByTask(userId, regionId, PlanetConstant.TASK_WAIMAI, PlanetConstant.TICKET_TYPE_ORDER, orderId, "外卖订单完成");
}
/**
* 团购订单完成发券
*/
public void onGroupFinish(String userId, String regionId, String groupOrderId) {
rewardByTask(userId, regionId, PlanetConstant.TASK_GROUP, PlanetConstant.TICKET_TYPE_GROUP, groupOrderId, "团购订单完成");
}
/**
* 邀请好友注册发券
*/
public void onInviteRegister(String inviterUserId, String regionId, String sourceId) {
rewardByTask(inviterUserId, regionId, PlanetConstant.TASK_INVITE, PlanetConstant.TICKET_TYPE_INVITE, sourceId, "邀请好友注册");
}
private void rewardByTask(String userId, String regionId, String taskCode, String type, String sourceId, String remark) {
if (StringUtils.isEmpty(userId)) {
return;
}
try {
ticketProducer.sendTaskReward(userId, regionId, taskCode, type, sourceId, remark);
} catch (Exception e) {
log.warn("[白嫖星球] 发券投递失败 userId={}, task={}, {}", userId, taskCode, e.getMessage());
}
}
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetBuffMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetBuff;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetBuffMapper extends BaseMapper<PlanetBuff> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetBuffRecordMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetBuffRecordMapper extends BaseMapper<PlanetBuffRecord> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetDrawRecordMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetDrawRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetDrawRecordMapper extends BaseMapper<PlanetDrawRecord> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetDrawWinnerMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetDrawWinner;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetDrawWinnerMapper extends BaseMapper<PlanetDrawWinner> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetHuntRecordMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetHuntRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetHuntRecordMapper extends BaseMapper<PlanetHuntRecord> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetNewsMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetNews;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetNewsMapper extends BaseMapper<PlanetNews> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetPoolMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetPool;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetPoolMapper extends BaseMapper<PlanetPool> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetRewardMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetReward;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetRewardMapper extends BaseMapper<PlanetReward> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTaskMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetTask;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetTaskMapper extends BaseMapper<PlanetTask> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTicketLogMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetTicketLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetTicketLogMapper extends BaseMapper<PlanetTicketLog> {
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mapper/PlanetTicketMapper.java

@ -0,0 +1,9 @@
package cc.hiver.mall.planet.mapper;
import cc.hiver.mall.planet.entity.PlanetTicket;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface PlanetTicketMapper extends BaseMapper<PlanetTicket> {
}

54
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetMqConfig.java

@ -0,0 +1,54 @@
package cc.hiver.mall.planet.mq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 白嫖星球 MQ 配置
* <p>
* 发券生成快讯等非核心动作异步化避免阻塞下单/注册等主业务流程
*
* @author hiver
*/
@Configuration
public class PlanetMqConfig {
public static final String PLANET_EXCHANGE = "planet.direct.exchange";
/** 发券队列 */
public static final String TICKET_GRANT_QUEUE = "planet.ticket.grant.queue";
public static final String TICKET_GRANT_ROUTING = "planet.ticket.grant.routing.key";
/** 快讯生成队列 */
public static final String NEWS_QUEUE = "planet.news.queue";
public static final String NEWS_ROUTING = "planet.news.routing.key";
@Bean
public DirectExchange planetDirectExchange() {
return new DirectExchange(PLANET_EXCHANGE, true, false);
}
@Bean
public Queue planetTicketGrantQueue() {
return new Queue(TICKET_GRANT_QUEUE, true);
}
@Bean
public Binding bindingPlanetTicketGrantQueue() {
return BindingBuilder.bind(planetTicketGrantQueue()).to(planetDirectExchange()).with(TICKET_GRANT_ROUTING);
}
@Bean
public Queue planetNewsQueue() {
return new Queue(NEWS_QUEUE, true);
}
@Bean
public Binding bindingPlanetNewsQueue() {
return BindingBuilder.bind(planetNewsQueue()).to(planetDirectExchange()).with(NEWS_ROUTING);
}
}

109
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetTicketConsumer.java

@ -0,0 +1,109 @@
package cc.hiver.mall.planet.mq;
import cc.hiver.mall.entity.MallOrder;
import cc.hiver.mall.planet.entity.PlanetTask;
import cc.hiver.mall.planet.service.PlanetNewsService;
import cc.hiver.mall.planet.service.PlanetTaskService;
import cc.hiver.mall.planet.service.PlanetTicketService;
import cc.hiver.mall.service.mybatis.MallOrderService;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 白嫖星球-发券/快讯异步消费者
* <p>
* 真正的发券 DB 操作在此完成addTickets 通过 sourceId 幂等可安全应对 MQ 至少一次投递
*
* @author hiver
*/
@Slf4j
@Component
public class PlanetTicketConsumer {
@Autowired
private PlanetTaskService taskService;
@Autowired
private PlanetTicketService ticketService;
@Autowired
private PlanetNewsService newsService;
@Autowired
private MallOrderService mallOrderService;
@RabbitListener(queues = PlanetMqConfig.TICKET_GRANT_QUEUE)
@Transactional(rollbackFor = Exception.class)
public void handleTicketGrant(String message) {
try {
final JSONObject json = JSONObject.parseObject(message);
final String userId = json.getString("userId");
String regionId = json.getString("regionId");
final String taskCode = json.getString("taskCode");
final String type = json.getString("type");
final String sourceId = json.getString("sourceId");
final String remark = json.getString("remark");
if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(taskCode)) {
log.warn("[白嫖星球MQ] 发券消息参数异常: {}", message);
return;
}
// 商圈强隔离:消息缺 regionId 时(如邀请注册),从用户最近一笔订单兜底解析,确保券归属正确商圈
if (StringUtils.isEmpty(regionId)) {
regionId = resolveRegionByUserOrder(userId);
}
if (StringUtils.isEmpty(regionId)) {
log.warn("[白嫖星球MQ] 无法确定用户商圈,跳过发券 userId={}, task={}", userId, taskCode);
return;
}
final PlanetTask task = taskService.getTaskConfig(regionId, taskCode);
if (task == null || task.getEnabled() == null || task.getEnabled() != 1) {
log.info("[白嫖星球MQ] 任务未启用,跳过发券 task={}", taskCode);
return;
}
final int reward = task.getRewardTickets() == null ? 1 : task.getRewardTickets();
final int added = ticketService.addTickets(userId, regionId, reward, type, sourceId, remark);
log.info("[白嫖星球MQ] 发券完成 userId={}, task={}, 实发={}", userId, taskCode, added);
} catch (Exception e) {
// 业务异常无需重投(重投也会再次失败),仅记录日志,进入死信/丢弃
log.error("[白嫖星球MQ] 发券消息处理异常: {}, msg={}", e.getMessage(), message, e);
throw new AmqpRejectAndDontRequeueException(e);
}
}
/**
* 从用户最近一笔带商圈的订单解析 regionId用于邀请注册等无商圈上下文的发券
*/
private String resolveRegionByUserOrder(String userId) {
try {
final MallOrder order = mallOrderService.getOne(new LambdaQueryWrapper<MallOrder>()
.select(MallOrder::getRegionId)
.eq(MallOrder::getUserId, userId)
.isNotNull(MallOrder::getRegionId)
.ne(MallOrder::getRegionId, "")
.orderByDesc(MallOrder::getCreateTime)
.last("limit 1"));
return order == null ? null : order.getRegionId();
} catch (Exception e) {
log.warn("[白嫖星球MQ] 解析用户商圈失败 userId={}, {}", userId, e.getMessage());
return null;
}
}
@RabbitListener(queues = PlanetMqConfig.NEWS_QUEUE)
public void handleNews(String message) {
try {
final JSONObject json = JSONObject.parseObject(message);
newsService.addNews(json.getString("regionId"), json.getString("type"), json.getString("content"), null, null);
} catch (Exception e) {
log.error("[白嫖星球MQ] 快讯消息处理异常: {}, msg={}", e.getMessage(), message, e);
throw new AmqpRejectAndDontRequeueException(e);
}
}
}

60
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/mq/PlanetTicketProducer.java

@ -0,0 +1,60 @@
package cc.hiver.mall.planet.mq;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 白嫖星球-发券/快讯异步消息生产者
* <p>
* 仅做轻量投递不做任何 DB 操作保证主业务流程不被发券逻辑拖慢
*
* @author hiver
*/
@Slf4j
@Component
public class PlanetTicketProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 投递一条按任务发券消息消费端负责解析任务配置并幂等发券
*/
public void sendTaskReward(String userId, String regionId, String taskCode, String type, String sourceId, String remark) {
Map<String, Object> msg = new HashMap<>(8);
msg.put("userId", userId);
msg.put("regionId", regionId);
msg.put("taskCode", taskCode);
msg.put("type", type);
msg.put("sourceId", sourceId);
msg.put("remark", remark);
try {
rabbitTemplate.convertAndSend(PlanetMqConfig.PLANET_EXCHANGE, PlanetMqConfig.TICKET_GRANT_ROUTING, JSON.toJSONString(msg));
log.info("[白嫖星球MQ] 已投递发券消息 userId={}, task={}, sourceId={}", userId, taskCode, sourceId);
} catch (Exception e) {
// 投递失败仅记录,绝不影响主业务
log.warn("[白嫖星球MQ] 发券消息投递失败 userId={}, task={}, {}", userId, taskCode, e.getMessage());
}
}
/**
* 投递一条快讯生成消息
*/
public void sendNews(String regionId, String type, String content) {
Map<String, Object> msg = new HashMap<>(4);
msg.put("regionId", regionId);
msg.put("type", type);
msg.put("content", content);
try {
rabbitTemplate.convertAndSend(PlanetMqConfig.PLANET_EXCHANGE, PlanetMqConfig.NEWS_ROUTING, JSON.toJSONString(msg));
} catch (Exception e) {
log.warn("[白嫖星球MQ] 快讯消息投递失败 regionId={}, {}", regionId, e.getMessage());
}
}
}

34
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetBoxResultVo.java

@ -0,0 +1,34 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 白嫖星球-宝箱开启结果
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-宝箱开启结果")
public class PlanetBoxResultVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "奖励类型 ticket券/buff增益")
private String rewardType;
@ApiModelProperty(value = "获得券数量(ticket时)")
private Integer ticketCount;
@ApiModelProperty(value = "BUFF类型(buff时)")
private String buffType;
@ApiModelProperty(value = "奖励名称")
private String rewardName;
@ApiModelProperty(value = "提示文案")
private String message;
}

51
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetBuffVo.java

@ -0,0 +1,51 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 白嫖星球-BUFF项(含当前是否生效)
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-BUFF项")
public class PlanetBuffVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "BUFF配置id")
private String id;
@ApiModelProperty(value = "类型")
private String type;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "描述")
private String description;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "消耗券数量")
private Integer costTickets;
@ApiModelProperty(value = "持续小时")
private Integer durationHours;
@ApiModelProperty(value = "效果值")
private BigDecimal effectValue;
@ApiModelProperty(value = "当前是否生效")
private Boolean active;
@ApiModelProperty(value = "当前生效到期时间")
private Date activeEndTime;
}

83
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetHomeVo.java

@ -0,0 +1,83 @@
package cc.hiver.mall.planet.pojo;
import cc.hiver.mall.planet.entity.PlanetNews;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 白嫖星球-首页聚合数据
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-首页聚合数据")
public class PlanetHomeVo implements Serializable {
private static final long serialVersionUID = 1L;
// ---------- 宇宙Header ----------
@ApiModelProperty(value = "本期奖金池")
private BigDecimal poolAmount;
@ApiModelProperty(value = "开奖时间")
private Date drawTime;
@ApiModelProperty(value = "距离开奖剩余毫秒")
private Long countdownMillis;
@ApiModelProperty(value = "本期参与人数")
private Integer joinCount;
@ApiModelProperty(value = "期号")
private String periodNo;
// ---------- 我的星球中心 ----------
@ApiModelProperty(value = "我的券数量")
private Integer myTicketCount;
@ApiModelProperty(value = "我的排名(0未上榜)")
private Integer myRankNo;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "连续签到天数")
private Integer consecutiveSignDays;
@ApiModelProperty(value = "今日是否已签到")
private Boolean signedToday;
@ApiModelProperty(value = "今日免费宝箱是否可开")
private Boolean boxAvailable;
@ApiModelProperty(value = "今日剩余追捕次数")
private Integer remainHunt;
@ApiModelProperty(value = "等级(危险等级)")
private String level;
@ApiModelProperty(value = "我当前生效的BUFF")
private List<PlanetBuffVo> myBuffs;
// ---------- 各板块 ----------
@ApiModelProperty(value = "任务列表")
private List<PlanetTaskVo> tasks;
@ApiModelProperty(value = "通缉榜TOP10")
private List<PlanetRankItemVo> rankList;
@ApiModelProperty(value = "BUFF商店")
private List<PlanetBuffVo> buffShop;
@ApiModelProperty(value = "星球快讯")
private List<PlanetNews> newsList;
}

40
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetHuntResultVo.java

@ -0,0 +1,40 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 白嫖星球-追捕结果
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-追捕结果")
public class PlanetHuntResultVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "结果 success缴获/shield对方护盾/fail失败")
private String result;
@ApiModelProperty(value = "缴获券")
private Integer gainTickets;
@ApiModelProperty(value = "悬赏额外券")
private Integer bountyTickets;
@ApiModelProperty(value = "本次合计获得券")
private Integer totalGain;
@ApiModelProperty(value = "目标昵称")
private String targetName;
@ApiModelProperty(value = "今日剩余追捕次数")
private Integer remainHunt;
@ApiModelProperty(value = "提示文案")
private String message;
}

52
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetQuery.java

@ -0,0 +1,52 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 白嫖星球-App端通用请求参数
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-App端通用请求参数")
public class PlanetQuery implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户id")
private String userId;
@ApiModelProperty(value = "商圈/校区id")
private String regionId;
@ApiModelProperty(value = "昵称(首次进入冗余)")
private String nickname;
@ApiModelProperty(value = "头像(首次进入冗余)")
private String avatar;
@ApiModelProperty(value = "学院")
private String college;
@ApiModelProperty(value = "目标用户id(追捕用)")
private String toUserId;
@ApiModelProperty(value = "任务编码")
private String taskCode;
@ApiModelProperty(value = "BUFF配置id")
private String buffId;
@ApiModelProperty(value = "中奖记录id")
private String winnerId;
@ApiModelProperty(value = "页码(从1开始)")
private Integer pageNumber;
@ApiModelProperty(value = "每页数量")
private Integer pageSize;
}

55
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetRankItemVo.java

@ -0,0 +1,55 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 白嫖星球-通缉榜单项
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-通缉榜单项")
public class PlanetRankItemVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "排名")
private Integer rankNo;
@ApiModelProperty(value = "用户id")
private String userId;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "学院")
private String college;
@ApiModelProperty(value = "当前券数量")
private Integer ticketCount;
@ApiModelProperty(value = "危险等级 C/B/A/S/SSS")
private String dangerLevel;
@ApiModelProperty(value = "危险等级名称")
private String dangerLevelName;
@ApiModelProperty(value = "连续霸榜天数")
private Integer rankKeepDays;
@ApiModelProperty(value = "悬赏额外券")
private Integer bountyTickets;
@ApiModelProperty(value = "是否开启防护罩")
private Boolean shielded;
@ApiModelProperty(value = "是否是自己")
private Boolean self;
}

46
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/pojo/PlanetTaskVo.java

@ -0,0 +1,46 @@
package cc.hiver.mall.planet.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 白嫖星球-任务项(含今日完成状态)
*
* @author hiver
*/
@Data
@ApiModel(value = "白嫖星球-任务项")
public class PlanetTaskVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "任务编码")
private String code;
@ApiModelProperty(value = "任务名称")
private String name;
@ApiModelProperty(value = "描述")
private String description;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "奖励券数量")
private Integer rewardTickets;
@ApiModelProperty(value = "每日上限 0不限")
private Integer dailyLimit;
@ApiModelProperty(value = "今日已完成次数")
private Integer todayCount;
@ApiModelProperty(value = "今日是否可领取(签到类)")
private Boolean canClaim;
@ApiModelProperty(value = "动作类型 claim可领取/auto订单自动")
private String actionType;
}

65
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetAdminService.java

@ -0,0 +1,65 @@
package cc.hiver.mall.planet.service;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.mall.planet.entity.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
/**
* 白嫖星球-后台管理服务
*
* @author hiver
*/
public interface PlanetAdminService {
// 奖池
Page<PlanetPool> pagePools(PageVo pageVo, String regionId, Integer status);
PlanetPool savePool(PlanetPool pool);
void deletePool(String id);
// 奖项
List<PlanetReward> rewardsOfPool(String poolId);
PlanetReward saveReward(PlanetReward reward);
void deleteReward(String id);
// 任务
Page<PlanetTask> pageTasks(PageVo pageVo, String regionId);
PlanetTask saveTask(PlanetTask task);
void deleteTask(String id);
// BUFF
Page<PlanetBuff> pageBuffs(PageVo pageVo, String regionId);
PlanetBuff saveBuff(PlanetBuff buff);
void deleteBuff(String id);
// 快讯
Page<PlanetNews> pageNews(PageVo pageVo, String regionId);
PlanetNews saveNews(PlanetNews news);
void deleteNews(String id);
// 排行榜(查看)
Page<PlanetTicket> pageRank(PageVo pageVo, String regionId);
// 追捕记录
Page<PlanetHuntRecord> pageHunts(PageVo pageVo, String regionId);
// 开奖记录
Page<PlanetDrawRecord> pageDraws(PageVo pageVo, String regionId);
// 中奖记录
Page<PlanetDrawWinner> pageWinners(PageVo pageVo, String regionId, String periodNo);
// 手动开奖
PlanetDrawRecord manualDraw(String poolId, String regionId);
}

21
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetBoxService.java

@ -0,0 +1,21 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.pojo.PlanetBoxResultVo;
/**
* 白嫖星球-幸运宝箱服务
*
* @author hiver
*/
public interface PlanetBoxService {
/**
* 开启每日免费宝箱
*/
PlanetBoxResultVo openDailyBox(String userId, String regionId);
/**
* 今日是否还可开宝箱
*/
boolean boxAvailable(String userId, String regionId);
}

63
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetBuffService.java

@ -0,0 +1,63 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.pojo.PlanetBuffVo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 白嫖星球-BUFF服务
*
* @author hiver
*/
public interface PlanetBuffService {
/**
* BUFF商店列表(含当前用户是否生效)
*/
List<PlanetBuffVo> shopList(String userId, String regionId);
/**
* 购买BUFF(消耗券并生效)
*/
PlanetBuffVo buyBuff(String userId, String regionId, String buffId);
/**
* 当前用户生效中的BUFF列表
*/
List<PlanetBuffRecord> activeBuffs(String userId);
/**
* 判断某类型BUFF是否生效
*/
boolean hasActiveBuff(String userId, String type);
/**
* 给用户授予一个BUFF(宝箱/自动防护使用)
*/
PlanetBuffRecord grantBuff(String userId, String regionId, String type, String source);
/**
* 幸运BUFF开奖权重加成系数(1+effectValue)无则1
*/
double luckyWeightFactor(String userId);
/**
* 批量查询多个用户当前生效中的 BUFF 类型集合(一次查库内存组装)
*
* @param userIds 用户id集合
* @return Map&lt;userId, Set&lt;buffType&gt;&gt;无生效BUFF的用户不在map中
*/
Map<String, Set<String>> activeBuffTypes(Collection<String> userIds);
/**
* 批量查询多个用户的幸运BUFF权重系数(一次查库内存组装)
*
* @param userIds 用户id集合
* @return Map&lt;userId, 系数(1+effectValue)&gt;无幸运BUFF的用户不在map中
*/
Map<String, Double> luckyWeightFactors(Collection<String> userIds);
}

52
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetDrawService.java

@ -0,0 +1,52 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetDrawRecord;
import cc.hiver.mall.planet.entity.PlanetDrawWinner;
import cc.hiver.mall.planet.entity.PlanetPool;
import java.util.List;
/**
* 白嫖星球-开奖服务
*
* @author hiver
*/
public interface PlanetDrawService {
/**
* 获取当前进行中的奖池(无则自动创建默认奖池)
*/
PlanetPool getCurrentPool(String regionId);
/**
* 执行开奖(加权抽取必中名单)
*
* @return 开奖记录
*/
PlanetDrawRecord drawPool(String poolId);
/**
* 扫描所有到点未开奖的奖池并开奖(定时任务用)
*/
int drawDuePools();
/**
* 最近一期开奖记录
*/
PlanetDrawRecord latestDrawRecord(String regionId);
/**
* 某次开奖的中奖名单
*/
List<PlanetDrawWinner> winnersOfDraw(String drawId);
/**
* 我的中奖记录(仅限本商圈)
*/
List<PlanetDrawWinner> myWinning(String userId, String regionId);
/**
* 领取中奖奖励(校验商圈一致)
*/
void receive(String userId, String regionId, String winnerId);
}

21
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetHuntService.java

@ -0,0 +1,21 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.pojo.PlanetHuntResultVo;
/**
* 白嫖星球-追捕服务
*
* @author hiver
*/
public interface PlanetHuntService {
/**
* 发起追捕
*/
PlanetHuntResultVo hunt(String fromUserId, String regionId, String toUserId);
/**
* 今日剩余追捕次数
*/
int remainHunt(String userId);
}

23
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetNewsService.java

@ -0,0 +1,23 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetNews;
import java.util.List;
/**
* 白嫖星球-星球快讯服务
*
* @author hiver
*/
public interface PlanetNewsService {
/**
* 新增快讯
*/
void addNews(String regionId, String type, String content, String userId, String userName);
/**
* 获取最新快讯
*/
List<PlanetNews> latest(String regionId, int limit);
}

29
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetRankService.java

@ -0,0 +1,29 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.pojo.PlanetRankItemVo;
import java.util.List;
/**
* 白嫖星球-通缉榜服务
*
* @author hiver
*/
public interface PlanetRankService {
/**
* 获取通缉榜TOP N(排除隐身BUFF用户)
*/
List<PlanetRankItemVo> rankTop(String regionId, int topN, String currentUserId);
/**
* 获取榜单内某用户的原始券记录(用于追捕悬赏计算)不在榜返回null
*/
PlanetTicket getRankTarget(String regionId, String userId);
/**
* 用户当前排名(0未上榜)
*/
int userRankNo(String regionId, String userId);
}

17
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetService.java

@ -0,0 +1,17 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.pojo.PlanetHomeVo;
import cc.hiver.mall.planet.pojo.PlanetQuery;
/**
* 白嫖星球-首页聚合服务
*
* @author hiver
*/
public interface PlanetService {
/**
* 首页聚合数据
*/
PlanetHomeVo home(PlanetQuery query);
}

41
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetTaskService.java

@ -0,0 +1,41 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetTask;
import cc.hiver.mall.planet.pojo.PlanetTaskVo;
import java.util.List;
/**
* 白嫖星球-任务服务
*
* @author hiver
*/
public interface PlanetTaskService {
/**
* 任务列表(含今日完成状态)
*/
List<PlanetTaskVo> listTasks(String userId, String regionId);
/**
* 每日签到
*
* @return 获得券数量
*/
int sign(String userId, String regionId);
/**
* 领取任务奖励(目前仅签到可主动领取)
*/
int claim(String userId, String regionId, String taskCode);
/**
* 根据编码获取生效任务配置
*/
PlanetTask getTaskConfig(String regionId, String code);
/**
* 今日是否已签到
*/
boolean signedToday(String userId, String regionId);
}

50
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/PlanetTicketService.java

@ -0,0 +1,50 @@
package cc.hiver.mall.planet.service;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.entity.PlanetTicketLog;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 白嫖星球-星球券服务
*
* @author hiver
*/
public interface PlanetTicketService {
/**
* 获取或创建用户券记录(自动补全昵称头像)
*/
PlanetTicket getOrCreate(String userId, String regionId, String nickname, String avatar, String college);
/**
* 查询用户券记录(不存在返回null)
*/
PlanetTicket getByUser(String userId, String regionId);
/**
* 增加星球券(支持双倍BUFF与幂等)
*
* @param baseCount 基础数量(BUFF前)
* @param type 来源类型
* @param sourceId 来源业务id(非空时做幂等去重)
* @return 实际增加数量(0表示被幂等拦截)
*/
int addTickets(String userId, String regionId, int baseCount, String type, String sourceId, String remark);
/**
* 扣减星球券(余额不足抛异常)
*
* @return 扣减后余额
*/
int deductTickets(String userId, String regionId, int count, String type, String sourceId, String remark);
/**
* 当前用户在商圈内的排名(0表示未上榜)
*/
int getRankNo(String regionId, String userId);
/**
* 分页查询用户星球券明细流水(按时间倒序)
*/
Page<PlanetTicketLog> pageLog(String userId, String regionId, Integer pageNumber, Integer pageSize);
}

288
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetAdminServiceImpl.java

@ -0,0 +1,288 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.mall.planet.entity.*;
import cc.hiver.mall.planet.mapper.*;
import cc.hiver.mall.planet.service.PlanetAdminService;
import cc.hiver.mall.planet.service.PlanetDrawService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 白嫖星球-后台管理服务实现
*
* @author hiver
*/
@Service
public class PlanetAdminServiceImpl implements PlanetAdminService {
@Autowired
private PlanetPoolMapper poolMapper;
@Autowired
private PlanetRewardMapper rewardMapper;
@Autowired
private PlanetTaskMapper taskMapper;
@Autowired
private PlanetBuffMapper buffMapper;
@Autowired
private PlanetNewsMapper newsMapper;
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetHuntRecordMapper huntRecordMapper;
@Autowired
private PlanetDrawRecordMapper drawRecordMapper;
@Autowired
private PlanetDrawWinnerMapper drawWinnerMapper;
@Autowired
private PlanetDrawService drawService;
private static String genId() {
return UUID.randomUUID().toString().replace("-", "");
}
private <T> Page<T> page(PageVo pageVo) {
final int num = pageVo == null || pageVo.getPageNumber() <= 0 ? 1 : pageVo.getPageNumber();
final int size = pageVo == null || pageVo.getPageSize() <= 0 ? 10 : pageVo.getPageSize();
return new Page<>(num, size);
}
@Override
public Page<PlanetPool> pagePools(PageVo pageVo, String regionId, Integer status) {
final LambdaQueryWrapper<PlanetPool> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetPool::getRegionId, regionId);
}
if (status != null) {
qw.eq(PlanetPool::getStatus, status);
}
qw.orderByDesc(PlanetPool::getCreateTime);
return poolMapper.selectPage(page(pageVo), qw);
}
@Override
public PlanetPool savePool(PlanetPool pool) {
if (StringUtils.isEmpty(pool.getId())) {
pool.setId(genId());
if (pool.getStatus() == null) {
pool.setStatus(0);
}
if (pool.getJoinCount() == null) {
pool.setJoinCount(0);
}
if (pool.getTicketTotal() == null) {
pool.setTicketTotal(0);
}
if (pool.getWinnerCount() == null) {
pool.setWinnerCount(0);
}
pool.setCreateTime(new Date());
pool.setUpdateTime(new Date());
poolMapper.insert(pool);
} else {
pool.setUpdateTime(new Date());
poolMapper.updateById(pool);
}
return pool;
}
@Override
public void deletePool(String id) {
poolMapper.deleteById(id);
final LambdaQueryWrapper<PlanetReward> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetReward::getPoolId, id);
rewardMapper.delete(qw);
}
@Override
public List<PlanetReward> rewardsOfPool(String poolId) {
final LambdaQueryWrapper<PlanetReward> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetReward::getPoolId, poolId).orderByAsc(PlanetReward::getSort);
return rewardMapper.selectList(qw);
}
@Override
public PlanetReward saveReward(PlanetReward reward) {
if (StringUtils.isEmpty(reward.getId())) {
reward.setId(genId());
reward.setCreateTime(new Date());
rewardMapper.insert(reward);
} else {
rewardMapper.updateById(reward);
}
return reward;
}
@Override
public void deleteReward(String id) {
rewardMapper.deleteById(id);
}
@Override
public Page<PlanetTask> pageTasks(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetTask> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTask::getRegionId, regionId);
}
qw.orderByAsc(PlanetTask::getSort);
return taskMapper.selectPage(page(pageVo), qw);
}
@Override
public PlanetTask saveTask(PlanetTask task) {
if (StringUtils.isEmpty(task.getId())) {
task.setId(genId());
if (task.getEnabled() == null) {
task.setEnabled(1);
}
task.setCreateTime(new Date());
taskMapper.insert(task);
} else {
taskMapper.updateById(task);
}
return task;
}
@Override
public void deleteTask(String id) {
taskMapper.deleteById(id);
}
@Override
public Page<PlanetBuff> pageBuffs(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetBuff> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetBuff::getRegionId, regionId);
}
qw.orderByAsc(PlanetBuff::getSort);
return buffMapper.selectPage(page(pageVo), qw);
}
@Override
public PlanetBuff saveBuff(PlanetBuff buff) {
if (StringUtils.isEmpty(buff.getId())) {
buff.setId(genId());
if (buff.getEnabled() == null) {
buff.setEnabled(1);
}
buff.setCreateTime(new Date());
buffMapper.insert(buff);
} else {
buffMapper.updateById(buff);
}
return buff;
}
@Override
public void deleteBuff(String id) {
buffMapper.deleteById(id);
}
@Override
public Page<PlanetNews> pageNews(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetNews> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetNews::getRegionId, regionId);
}
qw.orderByDesc(PlanetNews::getCreateTime);
return newsMapper.selectPage(page(pageVo), qw);
}
@Override
public PlanetNews saveNews(PlanetNews news) {
if (StringUtils.isEmpty(news.getId())) {
news.setId(genId());
if (news.getEnabled() == null) {
news.setEnabled(1);
}
if (news.getIsTop() == null) {
news.setIsTop(0);
}
news.setCreateTime(new Date());
newsMapper.insert(news);
} else {
newsMapper.updateById(news);
}
return news;
}
@Override
public void deleteNews(String id) {
newsMapper.deleteById(id);
}
@Override
public Page<PlanetTicket> pageRank(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.orderByDesc(PlanetTicket::getTicketCount);
return ticketMapper.selectPage(page(pageVo), qw);
}
@Override
public Page<PlanetHuntRecord> pageHunts(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetHuntRecord> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetHuntRecord::getRegionId, regionId);
}
qw.orderByDesc(PlanetHuntRecord::getCreateTime);
return huntRecordMapper.selectPage(page(pageVo), qw);
}
@Override
public Page<PlanetDrawRecord> pageDraws(PageVo pageVo, String regionId) {
final LambdaQueryWrapper<PlanetDrawRecord> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetDrawRecord::getRegionId, regionId);
}
qw.orderByDesc(PlanetDrawRecord::getDrawTime);
return drawRecordMapper.selectPage(page(pageVo), qw);
}
@Override
public Page<PlanetDrawWinner> pageWinners(PageVo pageVo, String regionId, String periodNo) {
final LambdaQueryWrapper<PlanetDrawWinner> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetDrawWinner::getRegionId, regionId);
}
if (StringUtils.isNotEmpty(periodNo)) {
qw.eq(PlanetDrawWinner::getPeriodNo, periodNo);
}
qw.orderByDesc(PlanetDrawWinner::getCreateTime);
return drawWinnerMapper.selectPage(page(pageVo), qw);
}
@Override
public PlanetDrawRecord manualDraw(String poolId, String regionId) {
if (StringUtils.isEmpty(regionId)) {
throw new HiverException("缺少商圈参数,开奖仅限本区域内进行");
}
final PlanetPool pool = poolMapper.selectById(poolId);
if (pool == null) {
throw new HiverException("奖池不存在");
}
if (!regionId.equals(pool.getRegionId())) {
throw new HiverException("无权对其它商圈的奖池开奖");
}
return drawService.drawPool(poolId);
}
}

107
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetBoxServiceImpl.java

@ -0,0 +1,107 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.mapper.PlanetTicketMapper;
import cc.hiver.mall.planet.pojo.PlanetBoxResultVo;
import cc.hiver.mall.planet.service.PlanetBoxService;
import cc.hiver.mall.planet.service.PlanetBuffService;
import cc.hiver.mall.planet.service.PlanetNewsService;
import cc.hiver.mall.planet.service.PlanetTicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* 白嫖星球-幸运宝箱服务实现
*
* @author hiver
*/
@Service
public class PlanetBoxServiceImpl implements PlanetBoxService {
@Autowired
private PlanetTicketService ticketService;
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetBuffService buffService;
@Autowired
private PlanetNewsService newsService;
@Override
public PlanetBoxResultVo openDailyBox(String userId, String regionId) {
final PlanetTicket ticket = ticketService.getOrCreate(userId, regionId, null, null, null);
final LocalDate today = LocalDate.now();
if (ticket.getBoxOpenedDate() != null && ticket.getBoxOpenedDate().isEqual(today)) {
throw new HiverException("今日宝箱已开启,明天再来");
}
// 标记已开
ticket.setBoxOpenedDate(today);
ticket.setUpdateTime(new Date());
ticketMapper.updateById(ticket);
// 抽取奖励:加权随机
// 1券50% / 2券22% / 5券8% / 幸运BUFF6% / 防护BUFF6% / 双倍BUFF5% / 掠夺BUFF3%
final int roll = ThreadLocalRandom.current().nextInt(100);
final PlanetBoxResultVo vo = new PlanetBoxResultVo();
if (roll < 50) {
grantTicket(userId, regionId, 1, vo, "幸运宝箱开出 1 张星球券");
} else if (roll < 72) {
grantTicket(userId, regionId, 2, vo, "幸运宝箱开出 2 张星球券");
} else if (roll < 80) {
grantTicket(userId, regionId, 5, vo, "幸运宝箱开出 5 张星球券");
} else if (roll < 86) {
grantBuff(userId, regionId, PlanetConstant.BUFF_LUCKY, "幸运星辰", vo);
} else if (roll < 92) {
grantBuff(userId, regionId, PlanetConstant.BUFF_SHIELD, "星际防护罩", vo);
} else if (roll < 97) {
grantBuff(userId, regionId, PlanetConstant.BUFF_DOUBLE, "双倍能量", vo);
} else {
grantBuff(userId, regionId, PlanetConstant.BUFF_HUNT, "追猎雷达", vo);
}
newsService.addNews(regionId, PlanetConstant.NEWS_BOX,
safeName(ticket.getNickname()) + " 幸运开启宝箱," + vo.getRewardName(),
userId, ticket.getNickname());
return vo;
}
@Override
public boolean boxAvailable(String userId, String regionId) {
final PlanetTicket ticket = ticketService.getByUser(userId, regionId);
if (ticket == null || ticket.getBoxOpenedDate() == null) {
return true;
}
return !ticket.getBoxOpenedDate().isEqual(LocalDate.now());
}
private void grantTicket(String userId, String regionId, int count, PlanetBoxResultVo vo, String name) {
ticketService.addTickets(userId, regionId, count, PlanetConstant.TICKET_TYPE_BOX,
UUID.randomUUID().toString().replace("-", ""), "宝箱奖励");
vo.setRewardType("ticket");
vo.setTicketCount(count);
vo.setRewardName("获得 " + count + " 张星球券");
vo.setMessage(name);
}
private void grantBuff(String userId, String regionId, String type, String name, PlanetBoxResultVo vo) {
buffService.grantBuff(userId, regionId, type, "box");
vo.setRewardType("buff");
vo.setBuffType(type);
vo.setRewardName("获得「" + name + "」增益");
vo.setMessage("幸运宝箱开出「" + name + "」增益");
}
private String safeName(String name) {
return name == null || name.isEmpty() ? "神秘同学" : name;
}
}

276
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetBuffServiceImpl.java

@ -0,0 +1,276 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetBuff;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.mapper.PlanetBuffMapper;
import cc.hiver.mall.planet.mapper.PlanetBuffRecordMapper;
import cc.hiver.mall.planet.pojo.PlanetBuffVo;
import cc.hiver.mall.planet.service.PlanetBuffService;
import cc.hiver.mall.planet.service.PlanetTicketService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
/**
* 白嫖星球-BUFF服务实现
*
* @author hiver
*/
@Service
public class PlanetBuffServiceImpl implements PlanetBuffService {
@Autowired
private PlanetBuffMapper buffMapper;
@Autowired
private PlanetBuffRecordMapper buffRecordMapper;
@Autowired
private PlanetTicketService ticketService;
@Override
public List<PlanetBuffVo> shopList(String userId, String regionId) {
final List<PlanetBuff> buffs = listEnabledBuffs(regionId);
final List<PlanetBuffRecord> actives = activeBuffs(userId);
final List<PlanetBuffVo> list = new ArrayList<>();
for (PlanetBuff buff : buffs) {
final PlanetBuffVo vo = toVo(buff);
for (PlanetBuffRecord r : actives) {
if (r.getType() != null && r.getType().equals(buff.getType())) {
vo.setActive(true);
vo.setActiveEndTime(r.getEndTime());
break;
}
}
list.add(vo);
}
return list;
}
@Override
public PlanetBuffVo buyBuff(String userId, String regionId, String buffId) {
final PlanetBuff buff = buffMapper.selectById(buffId);
if (buff == null || buff.getEnabled() == null || buff.getEnabled() != 1) {
throw new HiverException("该BUFF不存在或已下架");
}
if (hasActiveBuff(userId, buff.getType())) {
throw new HiverException("该增益正在生效中,请勿重复购买");
}
if (buff.getCostTickets() != null && buff.getCostTickets() > 0) {
ticketService.deductTickets(userId, regionId, buff.getCostTickets(),
PlanetConstant.TICKET_TYPE_BUFF, buff.getId() + ":" + System.currentTimeMillis(),
"购买BUFF-" + buff.getName());
}
final PlanetBuffRecord record = grantBuffInner(userId, regionId, buff, "buy");
final PlanetBuffVo vo = toVo(buff);
vo.setActive(true);
vo.setActiveEndTime(record.getEndTime());
return vo;
}
@Override
public List<PlanetBuffRecord> activeBuffs(String userId) {
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetBuffRecord::getUserId, userId)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date())
.orderByDesc(PlanetBuffRecord::getEndTime);
return buffRecordMapper.selectList(qw);
}
@Override
public boolean hasActiveBuff(String userId, String type) {
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetBuffRecord::getUserId, userId)
.eq(PlanetBuffRecord::getType, type)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date());
return buffRecordMapper.selectCount(qw) > 0;
}
@Override
public PlanetBuffRecord grantBuff(String userId, String regionId, String type, String source) {
final PlanetBuff buff = findBuffByType(regionId, type);
if (buff == null) {
final PlanetBuff temp = new PlanetBuff();
temp.setType(type);
temp.setDurationHours(PlanetConstant.AUTO_SHIELD_HOURS);
temp.setEffectValue(BigDecimal.ZERO);
return grantBuffInner(userId, regionId, temp, source);
}
return grantBuffInner(userId, regionId, buff, source);
}
@Override
public double luckyWeightFactor(String userId) {
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetBuffRecord::getUserId, userId)
.eq(PlanetBuffRecord::getType, PlanetConstant.BUFF_LUCKY)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date())
.orderByDesc(PlanetBuffRecord::getEffectValue)
.last("limit 1");
final List<PlanetBuffRecord> list = buffRecordMapper.selectList(qw);
if (list != null && !list.isEmpty() && list.get(0).getEffectValue() != null) {
return 1.0 + list.get(0).getEffectValue().doubleValue();
}
return 1.0;
}
@Override
public Map<String, Set<String>> activeBuffTypes(Collection<String> userIds) {
final Map<String, Set<String>> map = new HashMap<>();
if (userIds == null || userIds.isEmpty()) {
return map;
}
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.in(PlanetBuffRecord::getUserId, userIds)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date())
.select(PlanetBuffRecord::getUserId, PlanetBuffRecord::getType);
final List<PlanetBuffRecord> records = buffRecordMapper.selectList(qw);
for (PlanetBuffRecord r : records) {
map.computeIfAbsent(r.getUserId(), k -> new HashSet<>()).add(r.getType());
}
return map;
}
@Override
public Map<String, Double> luckyWeightFactors(Collection<String> userIds) {
final Map<String, Double> map = new HashMap<>();
if (userIds == null || userIds.isEmpty()) {
return map;
}
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.in(PlanetBuffRecord::getUserId, userIds)
.eq(PlanetBuffRecord::getType, PlanetConstant.BUFF_LUCKY)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date())
.select(PlanetBuffRecord::getUserId, PlanetBuffRecord::getEffectValue);
final List<PlanetBuffRecord> records = buffRecordMapper.selectList(qw);
for (PlanetBuffRecord r : records) {
if (r.getEffectValue() == null) {
continue;
}
final double factor = 1.0 + r.getEffectValue().doubleValue();
// 同类型可能多条,取最大加成
map.merge(r.getUserId(), factor, Math::max);
}
return map;
}
private PlanetBuffVo toVo(PlanetBuff buff) {
final PlanetBuffVo vo = new PlanetBuffVo();
vo.setId(buff.getId());
vo.setType(buff.getType());
vo.setName(buff.getName());
vo.setDescription(buff.getDescription());
vo.setIcon(buff.getIcon());
vo.setCostTickets(buff.getCostTickets());
vo.setDurationHours(buff.getDurationHours());
vo.setEffectValue(buff.getEffectValue());
vo.setActive(false);
return vo;
}
private PlanetBuffRecord grantBuffInner(String userId, String regionId, PlanetBuff buff, String source) {
final PlanetBuffRecord record = new PlanetBuffRecord();
record.setId(UUID.randomUUID().toString().replace("-", ""));
record.setUserId(userId);
record.setRegionId(regionId);
record.setBuffId(buff.getId());
record.setType(buff.getType());
record.setEffectValue(buff.getEffectValue() == null ? BigDecimal.ZERO : buff.getEffectValue());
final Date now = new Date();
record.setStartTime(now);
final int hours = buff.getDurationHours() == null ? PlanetConstant.AUTO_SHIELD_HOURS : buff.getDurationHours();
record.setEndTime(new Date(now.getTime() + (long) hours * 3600 * 1000));
record.setStatus(PlanetConstant.BUFF_STATUS_ACTIVE);
record.setSource(source);
record.setCreateTime(now);
buffRecordMapper.insert(record);
return record;
}
private PlanetBuff findBuffByType(String regionId, String type) {
// 商圈强隔离:仅查本商圈BUFF配置,不再回退全局默认
if (StringUtils.isEmpty(regionId)) {
return null;
}
PlanetBuff buff = selectRegionBuff(regionId, type);
if (buff == null) {
seedDefaultBuffs(regionId);
buff = selectRegionBuff(regionId, type);
}
return buff;
}
private PlanetBuff selectRegionBuff(String regionId, String type) {
final LambdaQueryWrapper<PlanetBuff> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetBuff::getType, type).eq(PlanetBuff::getEnabled, 1)
.eq(PlanetBuff::getRegionId, regionId).last("limit 1");
return buffMapper.selectOne(rq);
}
private List<PlanetBuff> listEnabledBuffs(String regionId) {
// 商圈强隔离:仅返回本商圈BUFF,首次访问自动初始化默认BUFF
if (StringUtils.isEmpty(regionId)) {
return new ArrayList<>();
}
List<PlanetBuff> list = selectRegionBuffs(regionId);
if (list.isEmpty()) {
seedDefaultBuffs(regionId);
list = selectRegionBuffs(regionId);
}
return list;
}
private List<PlanetBuff> selectRegionBuffs(String regionId) {
final LambdaQueryWrapper<PlanetBuff> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetBuff::getRegionId, regionId).eq(PlanetBuff::getEnabled, 1)
.orderByAsc(PlanetBuff::getSort);
final List<PlanetBuff> list = buffMapper.selectList(rq);
return list == null ? new ArrayList<>() : list;
}
/**
* 为指定商圈初始化默认BUFF配置(幂等 type 存在则跳过)
*/
private void seedDefaultBuffs(String regionId) {
if (StringUtils.isEmpty(regionId)) {
return;
}
seedBuff(regionId, PlanetConstant.BUFF_DOUBLE, "双倍能量", "24小时内获得星球券翻倍", 5, 24, new BigDecimal("2.00"), 1);
seedBuff(regionId, PlanetConstant.BUFF_SHIELD, "星际防护罩", "24小时内免疫追捕(养只小松鼠警卫)", 8, 24, BigDecimal.ZERO, 2);
seedBuff(regionId, PlanetConstant.BUFF_LUCKY, "幸运星辰", "开奖权重 +20%", 6, 24, new BigDecimal("0.20"), 3);
seedBuff(regionId, PlanetConstant.BUFF_HUNT, "追猎雷达", "追捕成功率提升", 4, 24, new BigDecimal("0.30"), 4);
seedBuff(regionId, PlanetConstant.BUFF_STEALTH, "隐身斗篷", "不出现在通缉榜", 6, 24, BigDecimal.ZERO, 5);
}
private void seedBuff(String regionId, String type, String name, String desc, int cost, int hours, BigDecimal effect, int sort) {
final LambdaQueryWrapper<PlanetBuff> exist = new LambdaQueryWrapper<>();
exist.eq(PlanetBuff::getRegionId, regionId).eq(PlanetBuff::getType, type);
if (buffMapper.selectCount(exist) > 0) {
return;
}
final PlanetBuff buff = new PlanetBuff();
buff.setId(UUID.randomUUID().toString().replace("-", ""));
buff.setRegionId(regionId);
buff.setType(type);
buff.setName(name);
buff.setDescription(desc);
buff.setCostTickets(cost);
buff.setDurationHours(hours);
buff.setEffectValue(effect);
buff.setSort(sort);
buff.setEnabled(1);
buff.setCreateTime(new Date());
buffMapper.insert(buff);
}
}

393
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java

@ -0,0 +1,393 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.*;
import cc.hiver.mall.planet.mapper.*;
import cc.hiver.mall.planet.service.PlanetBuffService;
import cc.hiver.mall.planet.service.PlanetDrawService;
import cc.hiver.mall.planet.service.PlanetNewsService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* 白嫖星球-开奖服务实现
*
* @author hiver
*/
@Slf4j
@Service
public class PlanetDrawServiceImpl implements PlanetDrawService {
/** 每日开奖时刻(24小时制),到点由定时任务自动开奖,每日一抽 */
private static final int DAILY_DRAW_HOUR = 22;
@Autowired
private PlanetPoolMapper poolMapper;
@Autowired
private PlanetRewardMapper rewardMapper;
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetDrawRecordMapper drawRecordMapper;
@Autowired
private PlanetDrawWinnerMapper drawWinnerMapper;
@Autowired
private PlanetBuffService buffService;
@Autowired
private PlanetNewsService newsService;
/**
* 抽奖功能强制按商圈隔离缺少 regionId 直接拒绝杜绝跨区域开奖/查询
*/
private void requireRegion(String regionId) {
if (StringUtils.isEmpty(regionId)) {
throw new HiverException("缺少商圈参数,抽奖仅限本区域内进行");
}
}
@Override
public PlanetPool getCurrentPool(String regionId) {
requireRegion(regionId);
final LambdaQueryWrapper<PlanetPool> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetPool::getRegionId, regionId)
.eq(PlanetPool::getStatus, PlanetConstant.POOL_STATUS_RUNNING)
.orderByDesc(PlanetPool::getCreateTime)
.last("limit 1");
PlanetPool pool = poolMapper.selectOne(qw);
if (pool == null) {
pool = createDefaultPool(regionId);
}
return pool;
}
private PlanetPool createDefaultPool(String regionId) {
final Date now = new Date();
final PlanetPool pool = new PlanetPool();
pool.setId(UUID.randomUUID().toString().replace("-", ""));
pool.setRegionId(regionId);
pool.setPeriodNo(genPeriodNo());
pool.setTitle("白嫖星球·本期瓜分");
pool.setPoolAmount(new BigDecimal("88.88"));
pool.setDrawTime(nextDrawTime(now));
pool.setJoinCount(0);
pool.setTicketTotal(0);
pool.setWinnerCount(0);
pool.setStatus(PlanetConstant.POOL_STATUS_RUNNING);
pool.setRemark("每日按星球券加权瓜分奖金池");
pool.setCreateTime(now);
pool.setUpdateTime(now);
poolMapper.insert(pool);
// 默认奖项
createDefaultRewards(pool);
return pool;
}
private void createDefaultRewards(PlanetPool pool) {
addReward(pool, 1, "一等奖", new BigDecimal("28.00"), 1, 1);
addReward(pool, 2, "二等奖", new BigDecimal("8.80"), 2, 2);
addReward(pool, 3, "三等奖", new BigDecimal("1.00"), 10, 3);
addReward(pool, 4, "幸运奖", new BigDecimal("0.50"), 50, 4);
}
private void addReward(PlanetPool pool, int level, String name, BigDecimal amount, int quota, int sort) {
final PlanetReward reward = new PlanetReward();
reward.setId(UUID.randomUUID().toString().replace("-", ""));
reward.setPoolId(pool.getId());
reward.setRegionId(pool.getRegionId());
reward.setLevel(level);
reward.setLevelName(name);
reward.setRewardType(0);
reward.setAmount(amount);
reward.setQuota(quota);
reward.setSort(sort);
reward.setCreateTime(new Date());
rewardMapper.insert(reward);
}
@Override
@Transactional(rollbackFor = Exception.class)
public PlanetDrawRecord drawPool(String poolId) {
final PlanetPool pool = poolMapper.selectById(poolId);
if (pool == null) {
throw new HiverException("奖池不存在");
}
if (pool.getStatus() != null && pool.getStatus() == PlanetConstant.POOL_STATUS_DRAWN) {
throw new HiverException("该期已开奖");
}
// 奖池必须归属某商圈,开奖只在本商圈内进行
requireRegion(pool.getRegionId());
// 参与者:仅本商圈内持券用户
final LambdaQueryWrapper<PlanetTicket> tq = new LambdaQueryWrapper<>();
tq.eq(PlanetTicket::getRegionId, pool.getRegionId())
.gt(PlanetTicket::getTicketCount, 0);
final List<PlanetTicket> participants = ticketMapper.selectList(tq);
// 奖项按层级排序
final LambdaQueryWrapper<PlanetReward> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetReward::getPoolId, poolId).orderByAsc(PlanetReward::getSort);
final List<PlanetReward> rewards = rewardMapper.selectList(rq);
// 批量查询所有参与者的幸运系数,一次查库,避免循环逐个查询
final Set<String> userIds = participants.stream().map(PlanetTicket::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
final Map<String, Double> luckyMap = buffService.luckyWeightFactors(userIds);
// 构建加权候选(权重= 券数量 * 幸运系数)
final List<Candidate> poutPool = new ArrayList<>();
int ticketTotal = 0;
for (PlanetTicket t : participants) {
final double factor = luckyMap.getOrDefault(t.getUserId(), 1.0);
final double weight = t.getTicketCount() * factor;
poutPool.add(new Candidate(t, weight));
ticketTotal += t.getTicketCount();
}
final Date now = new Date();
// 开奖记录
final PlanetDrawRecord record = new PlanetDrawRecord();
record.setId(UUID.randomUUID().toString().replace("-", ""));
record.setPoolId(pool.getId());
record.setRegionId(pool.getRegionId());
record.setPeriodNo(pool.getPeriodNo());
record.setPoolAmount(pool.getPoolAmount());
record.setJoinCount(participants.size());
record.setTicketTotal(ticketTotal);
record.setStatus(1);
record.setDrawTime(now);
record.setCreateTime(now);
int winnerCount = 0;
for (PlanetReward reward : rewards) {
final int quota = reward.getQuota() == null ? 0 : reward.getQuota();
for (int i = 0; i < quota; i++) {
if (poutPoolEmpty(poutPool)) {
break;
}
final Candidate winner = pickWeighted(poutPoolUsable(poutPool));
if (winner == null) {
break;
}
winner.win = true;
saveWinner(record, reward, winner.ticket, now);
winnerCount++;
}
}
record.setWinnerCount(winnerCount);
drawRecordMapper.insert(record);
// 更新奖池为已开奖
pool.setStatus(PlanetConstant.POOL_STATUS_DRAWN);
pool.setJoinCount(participants.size());
pool.setTicketTotal(ticketTotal);
pool.setWinnerCount(winnerCount);
pool.setUpdateTime(now);
poolMapper.updateById(pool);
// 快讯
newsService.addNews(pool.getRegionId(), PlanetConstant.NEWS_DRAW,
"本期奖池 ¥" + pool.getPoolAmount() + " 已开奖," + winnerCount + " 位同学瓜分成功",
null, null);
// 开启下一期
createNextPool(pool);
log.info("[白嫖星球] 开奖完成 poolId={}, 参与={}, 中奖={}", poolId, participants.size(), winnerCount);
return record;
}
private void createNextPool(PlanetPool prev) {
final Date now = new Date();
final PlanetPool pool = new PlanetPool();
pool.setId(UUID.randomUUID().toString().replace("-", ""));
pool.setRegionId(prev.getRegionId());
pool.setPeriodNo(genPeriodNo());
pool.setTitle(prev.getTitle());
pool.setPoolAmount(prev.getPoolAmount());
pool.setDrawTime(nextDrawTime(now));
pool.setJoinCount(0);
pool.setTicketTotal(0);
pool.setWinnerCount(0);
pool.setStatus(PlanetConstant.POOL_STATUS_RUNNING);
pool.setRemark(prev.getRemark());
pool.setCreateTime(now);
pool.setUpdateTime(now);
poolMapper.insert(pool);
// 复制上期奖项配置
final LambdaQueryWrapper<PlanetReward> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetReward::getPoolId, prev.getId()).orderByAsc(PlanetReward::getSort);
for (PlanetReward r : rewardMapper.selectList(rq)) {
addReward(pool, r.getLevel(), r.getLevelName(), r.getAmount(), r.getQuota() == null ? 0 : r.getQuota(), r.getSort() == null ? 0 : r.getSort());
}
}
@Override
public int drawDuePools() {
// 扫描所有到点未开奖的奖池:每个奖池都归属唯一校区,逐个独立开奖,
// 各校区参与者/奖项/奖金互不影响,实现“按校区分别开奖”。
final LambdaQueryWrapper<PlanetPool> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetPool::getStatus, PlanetConstant.POOL_STATUS_RUNNING)
.le(PlanetPool::getDrawTime, new Date());
final List<PlanetPool> due = poolMapper.selectList(qw);
int count = 0;
for (PlanetPool pool : due) {
// 防御:无校区归属的奖池不参与开奖,避免跨校区
if (StringUtils.isEmpty(pool.getRegionId())) {
log.warn("[白嫖星球] 跳过无校区奖池 poolId={}, periodNo={}", pool.getId(), pool.getPeriodNo());
continue;
}
try {
drawPool(pool.getId());
count++;
log.info("[白嫖星球] 校区开奖完成 regionId={}, periodNo={}", pool.getRegionId(), pool.getPeriodNo());
} catch (Exception e) {
log.error("[白嫖星球] 校区开奖失败 regionId={}, poolId={}, {}", pool.getRegionId(), pool.getId(), e.getMessage(), e);
}
}
return count;
}
@Override
public PlanetDrawRecord latestDrawRecord(String regionId) {
requireRegion(regionId);
final LambdaQueryWrapper<PlanetDrawRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetDrawRecord::getRegionId, regionId)
.orderByDesc(PlanetDrawRecord::getDrawTime).last("limit 1");
return drawRecordMapper.selectOne(qw);
}
@Override
public List<PlanetDrawWinner> winnersOfDraw(String drawId) {
final LambdaQueryWrapper<PlanetDrawWinner> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetDrawWinner::getDrawId, drawId).orderByAsc(PlanetDrawWinner::getRewardLevel);
return drawWinnerMapper.selectList(qw);
}
@Override
public List<PlanetDrawWinner> myWinning(String userId, String regionId) {
requireRegion(regionId);
final LambdaQueryWrapper<PlanetDrawWinner> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetDrawWinner::getUserId, userId)
.eq(PlanetDrawWinner::getRegionId, regionId)
.orderByDesc(PlanetDrawWinner::getCreateTime).last("limit 50");
return drawWinnerMapper.selectList(qw);
}
@Override
public void receive(String userId, String regionId, String winnerId) {
requireRegion(regionId);
final PlanetDrawWinner winner = drawWinnerMapper.selectById(winnerId);
if (winner == null || !userId.equals(winner.getUserId()) || !regionId.equals(winner.getRegionId())) {
throw new HiverException("中奖记录不存在");
}
if (winner.getIsReceived() != null && winner.getIsReceived() == 1) {
throw new HiverException("已领取,请勿重复操作");
}
winner.setIsReceived(1);
winner.setReceivedTime(new Date());
drawWinnerMapper.updateById(winner);
// TODO: 现金奖励可在此对接钱包入账;优惠券奖励可对接发券逻辑
}
private void saveWinner(PlanetDrawRecord record, PlanetReward reward, PlanetTicket ticket, Date now) {
final PlanetDrawWinner winner = new PlanetDrawWinner();
winner.setId(UUID.randomUUID().toString().replace("-", ""));
winner.setDrawId(record.getId());
winner.setPoolId(record.getPoolId());
winner.setRegionId(record.getRegionId());
winner.setPeriodNo(record.getPeriodNo());
winner.setUserId(ticket.getUserId());
winner.setUserName(ticket.getNickname());
winner.setRewardLevel(reward.getLevel());
winner.setLevelName(reward.getLevelName());
winner.setRewardType(reward.getRewardType());
winner.setAmount(reward.getAmount());
winner.setCouponId(reward.getCouponId());
winner.setIsReceived(0);
winner.setCreateTime(now);
drawWinnerMapper.insert(winner);
}
private boolean poutPoolEmpty(List<Candidate> pool) {
for (Candidate c : pool) {
if (!c.win) {
return false;
}
}
return true;
}
private List<Candidate> poutPoolUsable(List<Candidate> pool) {
final List<Candidate> usable = new ArrayList<>();
for (Candidate c : pool) {
if (!c.win) {
usable.add(c);
}
}
return usable;
}
private Candidate pickWeighted(List<Candidate> usable) {
if (usable.isEmpty()) {
return null;
}
double total = 0;
for (Candidate c : usable) {
total += Math.max(c.weight, 0.0001);
}
double r = ThreadLocalRandom.current().nextDouble() * total;
for (Candidate c : usable) {
r -= Math.max(c.weight, 0.0001);
if (r <= 0) {
return c;
}
}
return usable.get(usable.size() - 1);
}
private String genPeriodNo() {
return "P" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
}
/**
* 计算下一次开奖时间每日固定 {@link #DAILY_DRAW_HOUR} 点开奖已过则顺延到次日实现每日一抽
*/
private Date nextDrawTime(Date from) {
final Calendar c = Calendar.getInstance();
c.setTime(from);
c.set(Calendar.HOUR_OF_DAY, DAILY_DRAW_HOUR);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
if (!c.getTime().after(from)) {
c.add(Calendar.DAY_OF_MONTH, 1);
}
return c.getTime();
}
/** 开奖候选 */
private static class Candidate {
private final PlanetTicket ticket;
private final double weight;
private boolean win;
Candidate(PlanetTicket ticket, double weight) {
this.ticket = ticket;
this.weight = weight;
}
}
}

198
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetHuntServiceImpl.java

@ -0,0 +1,198 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.entity.PlanetHuntRecord;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.mapper.PlanetHuntRecordMapper;
import cc.hiver.mall.planet.pojo.PlanetHuntResultVo;
import cc.hiver.mall.planet.service.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* 白嫖星球-追捕服务实现
*
* @author hiver
*/
@Slf4j
@Service
public class PlanetHuntServiceImpl implements PlanetHuntService {
/** 基础追捕成功率 */
private static final double BASE_SUCCESS_RATE = 0.8;
@Autowired
private PlanetHuntRecordMapper huntRecordMapper;
@Autowired
private PlanetTicketService ticketService;
@Autowired
private PlanetRankService rankService;
@Autowired
private PlanetBuffService buffService;
@Autowired
private PlanetNewsService newsService;
@Override
public PlanetHuntResultVo hunt(String fromUserId, String regionId, String toUserId) {
if (StringUtils.isEmpty(fromUserId) || StringUtils.isEmpty(toUserId)) {
throw new HiverException("参数不完整");
}
if (fromUserId.equals(toUserId)) {
throw new HiverException("不能追捕自己");
}
final LocalDate today = LocalDate.now();
// 今日追捕次数校验
final int huntedToday = countTodayHunts(fromUserId, today);
if (huntedToday >= PlanetConstant.HUNT_DAILY_LIMIT) {
throw new HiverException("今日追捕次数已用完,明天再来");
}
final PlanetTicket from = ticketService.getOrCreate(fromUserId, regionId, null, null, null);
final PlanetTicket target = rankService.getRankTarget(regionId, toUserId);
if (target == null) {
throw new HiverException("目标不存在");
}
// 仅允许同区域互动
if (StringUtils.isNotEmpty(from.getRegionId()) && StringUtils.isNotEmpty(target.getRegionId())
&& !from.getRegionId().equals(target.getRegionId())) {
throw new HiverException("只能追捕同校区的同学");
}
final PlanetHuntResultVo vo = new PlanetHuntResultVo();
vo.setTargetName(target.getNickname());
vo.setGainTickets(0);
vo.setBountyTickets(0);
vo.setTotalGain(0);
// 目标防护罩:防护BUFF 或 今日已被追捕成功达上限
final int targetHuntedSuccess = countTodayHuntedSuccess(toUserId, today);
final boolean shielded = buffService.hasActiveBuff(toUserId, PlanetConstant.BUFF_SHIELD)
|| targetHuntedSuccess >= PlanetConstant.HUNTED_SUCCESS_LIMIT;
String result;
if (shielded) {
result = PlanetConstant.HUNT_SHIELD;
vo.setMessage("目标启动了星际防护罩,本次追捕未能缴获");
} else if (target.getTicketCount() == null || target.getTicketCount() <= 0) {
result = PlanetConstant.HUNT_FAIL;
vo.setMessage("目标星球空空如也,未能缴获");
} else {
// 成功率:基础 + 追捕BUFF加成
double rate = BASE_SUCCESS_RATE;
final PlanetBuffRecord huntBuff = firstActiveBuff(fromUserId, PlanetConstant.BUFF_HUNT);
if (huntBuff != null && huntBuff.getEffectValue() != null) {
rate += huntBuff.getEffectValue().doubleValue();
}
if (rate > 1) {
rate = 1;
}
final boolean success = ThreadLocalRandom.current().nextDouble() < rate;
if (success) {
result = PlanetConstant.HUNT_SUCCESS;
final int gain = 1;
// 计算目标当前排名的悬赏奖励
final int targetRank = rankService.userRankNo(regionId, toUserId);
final int bounty = PlanetConstant.bountyByRank(targetRank);
// 从目标缴获
ticketService.deductTickets(toUserId, target.getRegionId(), gain,
PlanetConstant.TICKET_TYPE_HUNT, UUID.randomUUID().toString().replace("-", ""),
"被" + safeName(from.getNickname()) + "追捕缴获");
// 发起人获得缴获券 + 悬赏券
ticketService.addTickets(fromUserId, regionId, gain,
PlanetConstant.TICKET_TYPE_HUNT, UUID.randomUUID().toString().replace("-", ""),
"追捕缴获" + safeName(target.getNickname()));
if (bounty > 0) {
ticketService.addTickets(fromUserId, regionId, bounty,
PlanetConstant.TICKET_TYPE_HUNT, UUID.randomUUID().toString().replace("-", ""),
"通缉悬赏奖励");
}
vo.setGainTickets(gain);
vo.setBountyTickets(bounty);
vo.setTotalGain(gain + bounty);
vo.setMessage("追捕成功!缴获 " + gain + " 张星球券" + (bounty > 0 ? ",额外获得 " + bounty + " 张悬赏券" : ""));
// 目标被追成功达上限自动开防护罩
if (targetHuntedSuccess + 1 >= PlanetConstant.HUNTED_SUCCESS_LIMIT) {
buffService.grantBuff(toUserId, target.getRegionId(), PlanetConstant.BUFF_SHIELD, "hunt");
}
// 快讯
newsService.addNews(regionId, PlanetConstant.NEWS_HUNT,
safeName(target.getNickname()) + " 遭遇追捕,损失 " + (gain) + " 张星球券",
toUserId, target.getNickname());
} else {
result = PlanetConstant.HUNT_FAIL;
vo.setMessage("目标飞船灵活机动,本次追捕扑空");
}
}
vo.setResult(result);
// 写追捕记录
saveHuntRecord(from, target, regionId, result, vo.getGainTickets(), vo.getBountyTickets(), today);
vo.setRemainHunt(Math.max(0, PlanetConstant.HUNT_DAILY_LIMIT - (huntedToday + 1)));
return vo;
}
@Override
public int remainHunt(String userId) {
final int used = countTodayHunts(userId, LocalDate.now());
return Math.max(0, PlanetConstant.HUNT_DAILY_LIMIT - used);
}
private int countTodayHunts(String userId, LocalDate date) {
final LambdaQueryWrapper<PlanetHuntRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetHuntRecord::getFromUserId, userId)
.eq(PlanetHuntRecord::getHuntDate, date);
return huntRecordMapper.selectCount(qw).intValue();
}
private int countTodayHuntedSuccess(String userId, LocalDate date) {
final LambdaQueryWrapper<PlanetHuntRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetHuntRecord::getToUserId, userId)
.eq(PlanetHuntRecord::getResult, PlanetConstant.HUNT_SUCCESS)
.eq(PlanetHuntRecord::getHuntDate, date);
return huntRecordMapper.selectCount(qw).intValue();
}
private PlanetBuffRecord firstActiveBuff(String userId, String type) {
for (PlanetBuffRecord r : buffService.activeBuffs(userId)) {
if (type.equals(r.getType())) {
return r;
}
}
return null;
}
private void saveHuntRecord(PlanetTicket from, PlanetTicket target, String regionId, String result,
int gain, int bounty, LocalDate date) {
final PlanetHuntRecord record = new PlanetHuntRecord();
record.setId(UUID.randomUUID().toString().replace("-", ""));
record.setFromUserId(from.getUserId());
record.setFromUserName(from.getNickname());
record.setToUserId(target.getUserId());
record.setToUserName(target.getNickname());
record.setRegionId(regionId);
record.setResult(result);
record.setGainTickets(gain);
record.setBountyTickets(bounty);
record.setHuntDate(date);
record.setCreateTime(new Date());
huntRecordMapper.insert(record);
}
private String safeName(String name) {
return StringUtils.isEmpty(name) ? "神秘同学" : name;
}
}

54
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetNewsServiceImpl.java

@ -0,0 +1,54 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.mall.planet.entity.PlanetNews;
import cc.hiver.mall.planet.mapper.PlanetNewsMapper;
import cc.hiver.mall.planet.service.PlanetNewsService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 白嫖星球-星球快讯服务实现
*
* @author hiver
*/
@Service
public class PlanetNewsServiceImpl implements PlanetNewsService {
@Autowired
private PlanetNewsMapper newsMapper;
@Override
public void addNews(String regionId, String type, String content, String userId, String userName) {
final PlanetNews news = new PlanetNews();
news.setId(UUID.randomUUID().toString().replace("-", ""));
news.setRegionId(regionId);
news.setType(type);
news.setContent(content);
news.setUserId(userId);
news.setUserName(userName);
news.setIsTop(0);
news.setEnabled(1);
news.setCreateTime(new Date());
newsMapper.insert(news);
}
@Override
public List<PlanetNews> latest(String regionId, int limit) {
// 商圈强隔离:仅返回本商圈快讯,不再混入全局快讯
if (StringUtils.isEmpty(regionId)) {
return new java.util.ArrayList<>();
}
final LambdaQueryWrapper<PlanetNews> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetNews::getEnabled, 1)
.eq(PlanetNews::getRegionId, regionId)
.orderByDesc(PlanetNews::getIsTop).orderByDesc(PlanetNews::getCreateTime)
.last("limit " + limit);
return newsMapper.selectList(qw);
}
}

157
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetRankServiceImpl.java

@ -0,0 +1,157 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.redis.RedisTemplateHelper;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.mapper.PlanetTicketMapper;
import cc.hiver.mall.planet.pojo.PlanetRankItemVo;
import cc.hiver.mall.planet.service.PlanetBuffService;
import cc.hiver.mall.planet.service.PlanetRankService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 白嫖星球-通缉榜服务实现
*
* @author hiver
*/
@Slf4j
@Service
public class PlanetRankServiceImpl implements PlanetRankService {
/** 通缉榜缓存前缀(基础榜单,不含 self 标记) */
private static final String RANK_CACHE_PREFIX = "planet:rank:";
/** 缓存有效期(秒),榜单允许短时弱一致,降低高频查库压力 */
private static final long RANK_CACHE_SECONDS = 30L;
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetBuffService buffService;
@Autowired
private RedisTemplateHelper redisTemplateHelper;
@Override
public List<PlanetRankItemVo> rankTop(String regionId, int topN, String currentUserId) {
List<PlanetRankItemVo> base = readCache(regionId, topN);
if (base == null) {
base = buildRankFromDb(regionId, topN);
writeCache(regionId, topN, base);
}
// self 标记与请求用户相关,缓存不存储,返回时按当前用户即时打标
for (PlanetRankItemVo vo : base) {
vo.setSelf(vo.getUserId() != null && vo.getUserId().equals(currentUserId));
}
return base;
}
/**
* 从数据库构建基础榜单先批量取候选券记录再一次性批量查 BUFF内存组装避免循环查库
*/
private List<PlanetRankItemVo> buildRankFromDb(String regionId, int topN) {
// 多取一些以便过滤隐身用户后仍能凑满topN
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.gt(PlanetTicket::getTicketCount, 0)
.orderByDesc(PlanetTicket::getTicketCount)
.orderByDesc(PlanetTicket::getTotalTicket)
.last("limit " + (topN + 20));
final List<PlanetTicket> tickets = ticketMapper.selectList(qw);
if (tickets.isEmpty()) {
return new ArrayList<>();
}
// 批量查询候选用户的生效 BUFF 类型,一次查库
final Set<String> userIds = tickets.stream().map(PlanetTicket::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
final Map<String, Set<String>> buffMap = buffService.activeBuffTypes(userIds);
final List<PlanetRankItemVo> result = new ArrayList<>();
int rankNo = 0;
for (PlanetTicket t : tickets) {
final Set<String> buffs = buffMap.get(t.getUserId());
// 隐身BUFF用户不出现在榜单
if (buffs != null && buffs.contains(PlanetConstant.BUFF_STEALTH)) {
continue;
}
rankNo++;
if (rankNo > topN) {
break;
}
final PlanetRankItemVo vo = new PlanetRankItemVo();
vo.setRankNo(rankNo);
vo.setUserId(t.getUserId());
vo.setNickname(t.getNickname());
vo.setAvatar(t.getAvatar());
vo.setCollege(t.getCollege());
vo.setTicketCount(t.getTicketCount());
vo.setDangerLevel(PlanetConstant.dangerLevel(t.getTicketCount()));
vo.setDangerLevelName(PlanetConstant.dangerLevelName(t.getTicketCount()));
vo.setRankKeepDays(t.getRankKeepDays() == null ? 0 : t.getRankKeepDays());
vo.setBountyTickets(PlanetConstant.bountyByRank(rankNo));
vo.setShielded(buffs != null && buffs.contains(PlanetConstant.BUFF_SHIELD));
vo.setSelf(false);
result.add(vo);
}
return result;
}
private List<PlanetRankItemVo> readCache(String regionId, int topN) {
try {
final String json = redisTemplateHelper.get(cacheKey(regionId, topN));
if (StringUtils.isNotEmpty(json)) {
return JSON.parseArray(json, PlanetRankItemVo.class);
}
} catch (Exception e) {
log.warn("[白嫖星球] 读取通缉榜缓存失败 {}", e.getMessage());
}
return null;
}
private void writeCache(String regionId, int topN, List<PlanetRankItemVo> list) {
try {
redisTemplateHelper.set(cacheKey(regionId, topN), JSON.toJSONString(list), RANK_CACHE_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[白嫖星球] 写入通缉榜缓存失败 {}", e.getMessage());
}
}
private String cacheKey(String regionId, int topN) {
return RANK_CACHE_PREFIX + (StringUtils.isEmpty(regionId) ? "all" : regionId) + ":" + topN;
}
@Override
public PlanetTicket getRankTarget(String regionId, String userId) {
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetTicket::getUserId, userId);
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.last("limit 1");
return ticketMapper.selectOne(qw);
}
@Override
public int userRankNo(String regionId, String userId) {
final PlanetTicket me = getRankTarget(regionId, userId);
if (me == null || me.getTicketCount() == null || me.getTicketCount() <= 0) {
return 0;
}
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.gt(PlanetTicket::getTicketCount, me.getTicketCount());
return ticketMapper.selectCount(qw).intValue() + 1;
}
}

104
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java

@ -0,0 +1,104 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.entity.PlanetPool;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.pojo.PlanetBuffVo;
import cc.hiver.mall.planet.pojo.PlanetHomeVo;
import cc.hiver.mall.planet.pojo.PlanetQuery;
import cc.hiver.mall.planet.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 白嫖星球-首页聚合服务实现
*
* @author hiver
*/
@Service
public class PlanetServiceImpl implements PlanetService {
@Autowired
private PlanetTicketService ticketService;
@Autowired
private PlanetRankService rankService;
@Autowired
private PlanetTaskService taskService;
@Autowired
private PlanetBuffService buffService;
@Autowired
private PlanetBoxService boxService;
@Autowired
private PlanetHuntService huntService;
@Autowired
private PlanetDrawService drawService;
@Autowired
private PlanetNewsService newsService;
@Override
public PlanetHomeVo home(PlanetQuery query) {
final String userId = query.getUserId();
final String regionId = query.getRegionId();
final PlanetHomeVo vo = new PlanetHomeVo();
// 初始化/补全用户券记录
final PlanetTicket ticket = ticketService.getOrCreate(userId, regionId,
query.getNickname(), query.getAvatar(), query.getCollege());
// 宇宙Header:当前奖池
final PlanetPool pool = drawService.getCurrentPool(regionId);
vo.setPoolAmount(pool.getPoolAmount() == null ? BigDecimal.ZERO : pool.getPoolAmount());
vo.setDrawTime(pool.getDrawTime());
vo.setPeriodNo(pool.getPeriodNo());
if (pool.getDrawTime() != null) {
vo.setCountdownMillis(Math.max(0, pool.getDrawTime().getTime() - new Date().getTime()));
} else {
vo.setCountdownMillis(0L);
}
vo.setJoinCount(pool.getJoinCount() == null ? 0 : pool.getJoinCount());
// 我的星球中心
vo.setMyTicketCount(ticket.getTicketCount() == null ? 0 : ticket.getTicketCount());
vo.setNickname(ticket.getNickname());
vo.setAvatar(ticket.getAvatar());
vo.setConsecutiveSignDays(ticket.getConsecutiveSignDays() == null ? 0 : ticket.getConsecutiveSignDays());
vo.setSignedToday(taskService.signedToday(userId, regionId));
vo.setBoxAvailable(boxService.boxAvailable(userId, regionId));
vo.setRemainHunt(huntService.remainHunt(userId));
vo.setLevel(PlanetConstant.dangerLevel(vo.getMyTicketCount()));
vo.setMyRankNo(rankService.userRankNo(regionId, userId));
// 我生效的BUFF
final List<PlanetBuffVo> myBuffs = new ArrayList<>();
for (PlanetBuffRecord r : buffService.activeBuffs(userId)) {
final PlanetBuffVo bv = new PlanetBuffVo();
bv.setId(r.getBuffId());
bv.setType(r.getType());
bv.setEffectValue(r.getEffectValue());
bv.setActive(true);
bv.setActiveEndTime(r.getEndTime());
myBuffs.add(bv);
}
vo.setMyBuffs(myBuffs);
// 各板块
vo.setTasks(taskService.listTasks(userId, regionId));
vo.setRankList(rankService.rankTop(regionId, 10, userId));
vo.setBuffShop(buffService.shopList(userId, regionId));
vo.setNewsList(newsService.latest(regionId, 20));
return vo;
}
}

199
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetTaskServiceImpl.java

@ -0,0 +1,199 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetTask;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.entity.PlanetTicketLog;
import cc.hiver.mall.planet.mapper.PlanetTaskMapper;
import cc.hiver.mall.planet.mapper.PlanetTicketLogMapper;
import cc.hiver.mall.planet.mapper.PlanetTicketMapper;
import cc.hiver.mall.planet.pojo.PlanetTaskVo;
import cc.hiver.mall.planet.service.PlanetNewsService;
import cc.hiver.mall.planet.service.PlanetTaskService;
import cc.hiver.mall.planet.service.PlanetTicketService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 白嫖星球-任务服务实现
*
* @author hiver
*/
@Service
public class PlanetTaskServiceImpl implements PlanetTaskService {
@Autowired
private PlanetTaskMapper taskMapper;
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetTicketLogMapper ticketLogMapper;
@Autowired
private PlanetTicketService ticketService;
@Autowired
private PlanetNewsService newsService;
@Override
public List<PlanetTaskVo> listTasks(String userId, String regionId) {
final List<PlanetTask> tasks = listEnabledTasks(regionId);
final List<PlanetTaskVo> list = new ArrayList<>();
for (PlanetTask task : tasks) {
final PlanetTaskVo vo = new PlanetTaskVo();
vo.setCode(task.getCode());
vo.setName(task.getName());
vo.setDescription(task.getDescription());
vo.setIcon(task.getIcon());
vo.setRewardTickets(task.getRewardTickets());
vo.setDailyLimit(task.getDailyLimit());
final int todayCount = countTodayByType(userId, task.getCode());
vo.setTodayCount(todayCount);
if (PlanetConstant.TASK_SIGN.equals(task.getCode())) {
vo.setActionType("claim");
vo.setCanClaim(!signedToday(userId, regionId));
} else {
vo.setActionType("auto");
vo.setCanClaim(false);
}
list.add(vo);
}
return list;
}
@Override
public int sign(String userId, String regionId) {
final PlanetTicket ticket = ticketService.getOrCreate(userId, regionId, null, null, null);
final LocalDate today = LocalDate.now();
if (ticket.getLastSignDate() != null && ticket.getLastSignDate().isEqual(today)) {
throw new HiverException("今日已签到");
}
// 连续签到天数
int days = 1;
if (ticket.getLastSignDate() != null && ticket.getLastSignDate().isEqual(today.minusDays(1))) {
days = (ticket.getConsecutiveSignDays() == null ? 0 : ticket.getConsecutiveSignDays()) + 1;
}
ticket.setConsecutiveSignDays(days);
ticket.setLastSignDate(today);
ticket.setUpdateTime(new Date());
ticketMapper.updateById(ticket);
final PlanetTask task = getTaskConfig(regionId, PlanetConstant.TASK_SIGN);
final int reward = task != null && task.getRewardTickets() != null ? task.getRewardTickets() : 1;
return ticketService.addTickets(userId, regionId, reward, PlanetConstant.TICKET_TYPE_SIGN,
"sign:" + userId + ":" + today, "每日签到");
}
@Override
public int claim(String userId, String regionId, String taskCode) {
if (PlanetConstant.TASK_SIGN.equals(taskCode)) {
return sign(userId, regionId);
}
throw new HiverException("该任务通过完成对应行为自动发放星球券");
}
@Override
public PlanetTask getTaskConfig(String regionId, String code) {
// 商圈强隔离:仅查本商圈任务配置,不再回退全局默认
if (StringUtils.isEmpty(regionId)) {
return null;
}
PlanetTask t = selectRegionTask(regionId, code);
if (t == null) {
seedDefaultTasks(regionId);
t = selectRegionTask(regionId, code);
}
return t;
}
private PlanetTask selectRegionTask(String regionId, String code) {
final LambdaQueryWrapper<PlanetTask> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetTask::getCode, code).eq(PlanetTask::getEnabled, 1)
.eq(PlanetTask::getRegionId, regionId).last("limit 1");
return taskMapper.selectOne(rq);
}
@Override
public boolean signedToday(String userId, String regionId) {
final PlanetTicket ticket = ticketService.getByUser(userId, regionId);
return ticket != null && ticket.getLastSignDate() != null && ticket.getLastSignDate().isEqual(LocalDate.now());
}
private List<PlanetTask> listEnabledTasks(String regionId) {
// 商圈强隔离:仅返回本商圈任务,首次访问自动初始化默认任务
if (StringUtils.isEmpty(regionId)) {
return new ArrayList<>();
}
List<PlanetTask> list = selectRegionTasks(regionId);
if (list.isEmpty()) {
seedDefaultTasks(regionId);
list = selectRegionTasks(regionId);
}
return list;
}
private List<PlanetTask> selectRegionTasks(String regionId) {
final LambdaQueryWrapper<PlanetTask> rq = new LambdaQueryWrapper<>();
rq.eq(PlanetTask::getRegionId, regionId).eq(PlanetTask::getEnabled, 1)
.orderByAsc(PlanetTask::getSort);
final List<PlanetTask> list = taskMapper.selectList(rq);
return list == null ? new ArrayList<>() : list;
}
/**
* 为指定商圈初始化默认任务配置(幂等 code 存在则跳过)
*/
private void seedDefaultTasks(String regionId) {
if (StringUtils.isEmpty(regionId)) {
return;
}
seedTask(regionId, PlanetConstant.TASK_WAIMAI, "完成外卖订单", "每完成一笔外卖订单 +1 星球券", 1, 0, 1);
seedTask(regionId, PlanetConstant.TASK_GROUP, "完成团购订单", "每完成一笔团购订单 +1 星球券", 1, 0, 2);
seedTask(regionId, PlanetConstant.TASK_INVITE, "邀请好友注册", "每邀请一位好友注册 +3 星球券", 3, 0, 3);
seedTask(regionId, PlanetConstant.TASK_SIGN, "每日签到", "每日签到领取 +1 星球券", 1, 1, 4);
}
private void seedTask(String regionId, String code, String name, String desc, int reward, int dailyLimit, int sort) {
final LambdaQueryWrapper<PlanetTask> exist = new LambdaQueryWrapper<>();
exist.eq(PlanetTask::getRegionId, regionId).eq(PlanetTask::getCode, code);
if (taskMapper.selectCount(exist) > 0) {
return;
}
final PlanetTask task = new PlanetTask();
task.setId(UUID.randomUUID().toString().replace("-", ""));
task.setRegionId(regionId);
task.setCode(code);
task.setName(name);
task.setDescription(desc);
task.setRewardTickets(reward);
task.setDailyLimit(dailyLimit);
task.setSort(sort);
task.setEnabled(1);
task.setCreateTime(new Date());
taskMapper.insert(task);
}
private int countTodayByType(String userId, String type) {
final LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
final Date startDate = Date.from(start.atZone(ZoneId.systemDefault()).toInstant());
final LambdaQueryWrapper<PlanetTicketLog> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetTicketLog::getUserId, userId)
.eq(PlanetTicketLog::getType, type)
.ge(PlanetTicketLog::getCreateTime, startDate);
return ticketLogMapper.selectCount(qw).intValue();
}
}

229
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetTicketServiceImpl.java

@ -0,0 +1,229 @@
package cc.hiver.mall.planet.service.impl;
import cc.hiver.core.common.exception.HiverException;
import cc.hiver.core.entity.User;
import cc.hiver.core.service.UserService;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.entity.PlanetTicketLog;
import cc.hiver.mall.planet.mapper.PlanetBuffRecordMapper;
import cc.hiver.mall.planet.mapper.PlanetTicketLogMapper;
import cc.hiver.mall.planet.mapper.PlanetTicketMapper;
import cc.hiver.mall.planet.service.PlanetTicketService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 白嫖星球-星球券服务实现
*
* @author hiver
*/
@Slf4j
@Service
public class PlanetTicketServiceImpl implements PlanetTicketService {
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetTicketLogMapper ticketLogMapper;
@Autowired
private PlanetBuffRecordMapper buffRecordMapper;
@Autowired
private UserService userService;
@Override
public PlanetTicket getOrCreate(String userId, String regionId, String nickname, String avatar, String college) {
if (StringUtils.isEmpty(userId)) {
throw new HiverException("用户id不能为空");
}
PlanetTicket ticket = getByUser(userId, regionId);
if (ticket == null) {
ticket = new PlanetTicket();
ticket.setId(UUID.randomUUID().toString().replace("-", ""));
ticket.setUserId(userId);
ticket.setRegionId(regionId);
ticket.setTicketCount(0);
ticket.setTotalTicket(0);
ticket.setConsecutiveSignDays(0);
ticket.setRankKeepDays(0);
ticket.setLastRankNo(0);
ticket.setHuntCountToday(0);
ticket.setHuntedSuccessToday(0);
fillUserInfo(ticket, nickname, avatar, college);
ticket.setCreateTime(new Date());
ticket.setUpdateTime(new Date());
ticketMapper.insert(ticket);
} else {
// 补全冗余信息
boolean needUpdate = false;
if (StringUtils.isEmpty(ticket.getNickname()) && StringUtils.isNotEmpty(nickname)) {
ticket.setNickname(nickname);
needUpdate = true;
}
if (StringUtils.isEmpty(ticket.getAvatar()) && StringUtils.isNotEmpty(avatar)) {
ticket.setAvatar(avatar);
needUpdate = true;
}
if (StringUtils.isEmpty(ticket.getCollege()) && StringUtils.isNotEmpty(college)) {
ticket.setCollege(college);
needUpdate = true;
}
if (needUpdate) {
ticket.setUpdateTime(new Date());
ticketMapper.updateById(ticket);
}
}
return ticket;
}
private void fillUserInfo(PlanetTicket ticket, String nickname, String avatar, String college) {
ticket.setNickname(nickname);
ticket.setAvatar(avatar);
ticket.setCollege(college);
if (StringUtils.isEmpty(nickname) || StringUtils.isEmpty(avatar)) {
try {
final User user = userService.findById(ticket.getUserId());
if (user != null) {
if (StringUtils.isEmpty(ticket.getNickname())) {
ticket.setNickname(user.getNickname());
}
if (StringUtils.isEmpty(ticket.getAvatar())) {
ticket.setAvatar(user.getAvatar());
}
}
} catch (Exception e) {
log.warn("[白嫖星球] 获取用户信息失败 userId={}, {}", ticket.getUserId(), e.getMessage());
}
}
}
@Override
public PlanetTicket getByUser(String userId, String regionId) {
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetTicket::getUserId, userId);
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.last("limit 1");
return ticketMapper.selectOne(qw);
}
@Override
public int addTickets(String userId, String regionId, int baseCount, String type, String sourceId, String remark) {
if (baseCount <= 0) {
return 0;
}
// 幂等:同一来源业务id只发放一次
if (StringUtils.isNotEmpty(sourceId)) {
final LambdaQueryWrapper<PlanetTicketLog> exist = new LambdaQueryWrapper<>();
exist.eq(PlanetTicketLog::getType, type)
.eq(PlanetTicketLog::getSourceId, sourceId);
if (ticketLogMapper.selectCount(exist) > 0) {
log.info("[白嫖星球] 重复发券已拦截 type={}, sourceId={}", type, sourceId);
return 0;
}
}
final PlanetTicket ticket = getOrCreate(userId, regionId, null, null, null);
// 双倍BUFF加成
int finalCount = baseCount;
final BigDecimal multiplier = getActiveDoubleMultiplier(userId);
if (multiplier != null && multiplier.compareTo(BigDecimal.ONE) > 0) {
finalCount = multiplier.multiply(BigDecimal.valueOf(baseCount)).intValue();
}
ticket.setTicketCount(ticket.getTicketCount() + finalCount);
ticket.setTotalTicket(ticket.getTotalTicket() + finalCount);
ticket.setUpdateTime(new Date());
ticketMapper.updateById(ticket);
saveLog(userId, regionId, finalCount, ticket.getTicketCount(), type, sourceId, remark);
return finalCount;
}
@Override
public int deductTickets(String userId, String regionId, int count, String type, String sourceId, String remark) {
if (count <= 0) {
throw new HiverException("扣减数量必须大于0");
}
final PlanetTicket ticket = getOrCreate(userId, regionId, null, null, null);
if (ticket.getTicketCount() < count) {
throw new HiverException("星球券不足");
}
ticket.setTicketCount(ticket.getTicketCount() - count);
ticket.setUpdateTime(new Date());
ticketMapper.updateById(ticket);
saveLog(userId, regionId, -count, ticket.getTicketCount(), type, sourceId, remark);
return ticket.getTicketCount();
}
@Override
public int getRankNo(String regionId, String userId) {
final PlanetTicket me = getByUser(userId, regionId);
if (me == null || me.getTicketCount() == null || me.getTicketCount() <= 0) {
return 0;
}
// 比我券多的人数 + 1 即为排名
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicket::getRegionId, regionId);
}
qw.gt(PlanetTicket::getTicketCount, me.getTicketCount());
final Long greater = ticketMapper.selectCount(qw);
return greater.intValue() + 1;
}
@Override
public Page<PlanetTicketLog> pageLog(String userId, String regionId, Integer pageNumber, Integer pageSize) {
final int num = pageNumber == null || pageNumber <= 0 ? 1 : pageNumber;
final int size = pageSize == null || pageSize <= 0 ? 10 : Math.min(pageSize, 50);
final Page<PlanetTicketLog> page = new Page<>(num, size);
final LambdaQueryWrapper<PlanetTicketLog> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetTicketLog::getUserId, userId);
if (StringUtils.isNotEmpty(regionId)) {
qw.eq(PlanetTicketLog::getRegionId, regionId);
}
qw.orderByDesc(PlanetTicketLog::getCreateTime);
return ticketLogMapper.selectPage(page, qw);
}
private BigDecimal getActiveDoubleMultiplier(String userId) {
final LambdaQueryWrapper<PlanetBuffRecord> qw = new LambdaQueryWrapper<>();
qw.eq(PlanetBuffRecord::getUserId, userId)
.eq(PlanetBuffRecord::getType, PlanetConstant.BUFF_DOUBLE)
.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.gt(PlanetBuffRecord::getEndTime, new Date())
.orderByDesc(PlanetBuffRecord::getEffectValue)
.last("limit 1");
final List<PlanetBuffRecord> list = buffRecordMapper.selectList(qw);
if (list != null && !list.isEmpty()) {
final BigDecimal v = list.get(0).getEffectValue();
return v == null ? BigDecimal.valueOf(2) : v;
}
return BigDecimal.ONE;
}
private void saveLog(String userId, String regionId, int change, int balance, String type, String sourceId, String remark) {
final PlanetTicketLog logEntity = new PlanetTicketLog();
logEntity.setId(UUID.randomUUID().toString().replace("-", ""));
logEntity.setUserId(userId);
logEntity.setRegionId(regionId);
logEntity.setChangeCount(change);
logEntity.setBalance(balance);
logEntity.setType(type);
logEntity.setSourceId(sourceId);
logEntity.setRemark(remark);
logEntity.setCreateTime(new Date());
ticketLogMapper.insert(logEntity);
}
}

86
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/PlanetDailyResetTask.java

@ -0,0 +1,86 @@
package cc.hiver.mall.quartz;
import cc.hiver.mall.planet.constant.PlanetConstant;
import cc.hiver.mall.planet.entity.PlanetBuffRecord;
import cc.hiver.mall.planet.entity.PlanetTicket;
import cc.hiver.mall.planet.mapper.PlanetBuffRecordMapper;
import cc.hiver.mall.planet.mapper.PlanetTicketMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
/**
* 白嫖星球-每日重置定时任务
* <p>
* 每日 00:05 执行
* 1. 过期BUFF置为失效状态
* 2. 重置今日追捕/被追计数(展示用)
* 3. 更新连续霸榜天数
*
* @author hiver
*/
@Slf4j
@Component
public class PlanetDailyResetTask {
@Autowired
private PlanetTicketMapper ticketMapper;
@Autowired
private PlanetBuffRecordMapper buffRecordMapper;
@Scheduled(cron = "0 5 0 * * ?")
public void dailyReset() {
try {
// 1. 过期BUFF
final LambdaUpdateWrapper<PlanetBuffRecord> bw = new LambdaUpdateWrapper<>();
bw.eq(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_ACTIVE)
.lt(PlanetBuffRecord::getEndTime, new Date())
.set(PlanetBuffRecord::getStatus, PlanetConstant.BUFF_STATUS_EXPIRED);
buffRecordMapper.update(null, bw);
// 2. 重置每日计数(展示字段)
final LambdaUpdateWrapper<PlanetTicket> tw = new LambdaUpdateWrapper<>();
tw.set(PlanetTicket::getHuntCountToday, 0)
.set(PlanetTicket::getHuntedSuccessToday, 0);
ticketMapper.update(null, tw);
// 3. 更新连续霸榜天数:仍在TOP10的用户 keepDays+1,否则清零
updateRankKeepDays();
log.info("[白嫖星球] 每日重置任务完成");
} catch (Exception e) {
log.error("[白嫖星球] 每日重置任务异常: {}", e.getMessage(), e);
}
}
private void updateRankKeepDays() {
// 取所有持券用户,按区域分别计算前10
final LambdaQueryWrapper<PlanetTicket> qw = new LambdaQueryWrapper<>();
qw.gt(PlanetTicket::getTicketCount, 0).orderByDesc(PlanetTicket::getTicketCount);
final List<PlanetTicket> all = ticketMapper.selectList(qw);
// 简化处理:全局/各区域统一按券数排序后,记录其在本区域的名次
// 这里仅对每个用户基于其当前是否进入本区域TOP10更新keepDays
final java.util.Map<String, Integer> regionRankCounter = new java.util.HashMap<>();
for (PlanetTicket t : all) {
final String region = t.getRegionId() == null ? "" : t.getRegionId();
final int rank = regionRankCounter.merge(region, 1, Integer::sum);
int keep = t.getRankKeepDays() == null ? 0 : t.getRankKeepDays();
if (rank <= 10) {
keep = keep + 1;
t.setLastRankNo(rank);
} else {
keep = 0;
t.setLastRankNo(0);
}
t.setRankKeepDays(keep);
t.setUpdateTime(new Date());
ticketMapper.updateById(t);
}
}
}

33
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/PlanetDrawTask.java

@ -0,0 +1,33 @@
package cc.hiver.mall.quartz;
import cc.hiver.mall.planet.service.PlanetDrawService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 白嫖星球-到点开奖定时任务
* 每分钟扫描到达开奖时间且未开奖的奖池并执行加权开奖
*
* @author hiver
*/
@Slf4j
@Component
public class PlanetDrawTask {
@Autowired
private PlanetDrawService drawService;
@Scheduled(cron = "0 * * * * ?")
public void drawDuePools() {
try {
final int count = drawService.drawDuePools();
if (count > 0) {
log.info("[白嫖星球] 本轮开奖奖池数量={}", count);
}
} catch (Exception e) {
log.error("[白嫖星球] 开奖任务异常: {}", e.getMessage(), e);
}
}
}

5
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java

@ -184,6 +184,9 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
@Autowired
private IeMatchService ieMatchService;
@Autowired
private cc.hiver.mall.planet.hook.PlanetRewardHook planetRewardHook;
// ================================================================
// 核心下单逻辑
// ================================================================
@ -1186,6 +1189,8 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
mallUserCouponService.useCoupon(orderId);
updateOrderStatus(orderId, STATUS_DONE);
rewardIeMatchQuota(order.getUserId());
//白嫖星球-订单完成发放星球券(不影响主流程)
planetRewardHook.onOrderFinish(order.getUserId(), order.getRegionId(), order.getId());
//更新缓存
userPendingOrderCacheUtil.remove(order.getUserId(),order.getId());
//免单计数(Key 按 regionId 隔离,避免多区域计数混乱)

2
hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_persona_images.sql

@ -0,0 +1,2 @@
ALTER TABLE ie_user_profile
ADD COLUMN persona_images VARCHAR(3000) NULL COMMENT '人格卡片图片,JSON数组,最多5张';

244
hiver-modules/hiver-mall/src/main/resources/sql/planet.sql

@ -0,0 +1,244 @@
-- =============================================================
-- 白嫖星球 游戏化福利系统 建表脚本
-- 模块前缀:t_planet_*
-- 主键统一 varchar(64),时间字段统一 datetime
-- =============================================================
-- 1. 奖池/期次表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_pool`;
CREATE TABLE `t_planet_pool` (
`id` varchar(64) NOT NULL COMMENT '主键',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈/校区id',
`period_no` varchar(64) DEFAULT NULL COMMENT '期号',
`title` varchar(100) DEFAULT NULL COMMENT '本期标题',
`pool_amount` decimal(10,2) DEFAULT '0.00' COMMENT '奖池总金额(元)',
`draw_time` datetime DEFAULT NULL COMMENT '开奖时间',
`join_count` int DEFAULT '0' COMMENT '参与人数',
`ticket_total` int DEFAULT '0' COMMENT '本期投入券总数',
`winner_count` int DEFAULT '0' COMMENT '中奖人数',
`status` int DEFAULT '0' COMMENT '状态 0进行中 1已开奖',
`remark` varchar(255) DEFAULT NULL COMMENT '备注/公告',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_region` (`region_id`),
KEY `idx_region_period` (`region_id`,`period_no`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-奖池期次';
-- 2. 奖项配置表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_reward`;
CREATE TABLE `t_planet_reward` (
`id` varchar(64) NOT NULL COMMENT '主键',
`pool_id` varchar(64) DEFAULT NULL COMMENT '奖池id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`level` int DEFAULT NULL COMMENT '奖项层级 1一等 2二等 3三等 4幸运',
`level_name` varchar(50) DEFAULT NULL COMMENT '奖项名称',
`reward_type` int DEFAULT '0' COMMENT '奖励类型 0现金 1优惠券',
`amount` decimal(10,2) DEFAULT '0.00' COMMENT '单份金额(元)',
`coupon_id` varchar(64) DEFAULT NULL COMMENT '优惠券id',
`quota` int DEFAULT '0' COMMENT '名额数量',
`sort` int DEFAULT '0' COMMENT '排序',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_pool` (`pool_id`),
KEY `idx_region` (`region_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-奖项配置';
-- 3. 用户券汇总表 -----------------------------------------------
DROP TABLE IF EXISTS `t_planet_ticket`;
CREATE TABLE `t_planet_ticket` (
`id` varchar(64) NOT NULL COMMENT '主键',
`user_id` varchar(64) NOT NULL COMMENT '用户id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`nickname` varchar(100) DEFAULT NULL COMMENT '昵称冗余',
`avatar` varchar(500) DEFAULT NULL COMMENT '头像冗余',
`college` varchar(100) DEFAULT NULL COMMENT '学院',
`ticket_count` int DEFAULT '0' COMMENT '当前可用券数量',
`total_ticket` int DEFAULT '0' COMMENT '历史累计获得券',
`consecutive_sign_days` int DEFAULT '0' COMMENT '连续签到天数',
`last_sign_date` date DEFAULT NULL COMMENT '最近签到日期',
`rank_keep_days` int DEFAULT '0' COMMENT '连续霸榜天数',
`last_rank_no` int DEFAULT '0' COMMENT '上次排名',
`hunt_count_today` int DEFAULT '0' COMMENT '今日已追捕次数',
`hunted_success_today` int DEFAULT '0' COMMENT '今日被追捕成功次数',
`box_opened_date` date DEFAULT NULL COMMENT '最近开宝箱日期',
`period_no` varchar(64) DEFAULT NULL COMMENT '所属期号',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_region` (`user_id`,`region_id`),
KEY `idx_region_ticket` (`region_id`,`ticket_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-用户券汇总';
-- 4. 券流水表 ---------------------------------------------------
DROP TABLE IF EXISTS `t_planet_ticket_log`;
CREATE TABLE `t_planet_ticket_log` (
`id` varchar(64) NOT NULL COMMENT '主键',
`user_id` varchar(64) NOT NULL COMMENT '用户id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`change_count` int DEFAULT '0' COMMENT '变更数量(正得负扣)',
`balance` int DEFAULT '0' COMMENT '变更后余额',
`type` varchar(20) DEFAULT NULL COMMENT '来源 order/group/invite/sign/box/hunt/buff/draw',
`source_id` varchar(64) DEFAULT NULL COMMENT '来源业务id(幂等)',
`period_no` varchar(64) DEFAULT NULL COMMENT '期号',
`remark` varchar(255) DEFAULT NULL COMMENT '说明',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_source` (`type`,`source_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-券流水';
-- 5. 任务规则表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_task`;
CREATE TABLE `t_planet_task` (
`id` varchar(64) NOT NULL COMMENT '主键',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id 空为全局默认',
`code` varchar(20) DEFAULT NULL COMMENT '任务编码 waimai/group/invite/sign',
`name` varchar(50) DEFAULT NULL COMMENT '任务名称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`icon` varchar(500) DEFAULT NULL COMMENT '图标',
`reward_tickets` int DEFAULT '1' COMMENT '奖励券数量',
`daily_limit` int DEFAULT '0' COMMENT '每日上限 0不限',
`sort` int DEFAULT '0' COMMENT '排序',
`enabled` int DEFAULT '1' COMMENT '是否启用 1是 0否',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-任务规则';
-- 6. BUFF配置表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_buff`;
CREATE TABLE `t_planet_buff` (
`id` varchar(64) NOT NULL COMMENT '主键',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id 空为全局默认',
`type` varchar(20) DEFAULT NULL COMMENT '类型 double/shield/lucky/hunt/stealth',
`name` varchar(50) DEFAULT NULL COMMENT '名称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`icon` varchar(500) DEFAULT NULL COMMENT '图标',
`cost_tickets` int DEFAULT '0' COMMENT '消耗券数量',
`duration_hours` int DEFAULT '24' COMMENT '持续小时',
`effect_value` decimal(10,2) DEFAULT '0.00' COMMENT '效果值(如加权0.2)',
`sort` int DEFAULT '0' COMMENT '排序',
`enabled` int DEFAULT '1' COMMENT '是否启用 1是 0否',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-BUFF配置';
-- 7. 用户BUFF记录表 ---------------------------------------------
DROP TABLE IF EXISTS `t_planet_buff_record`;
CREATE TABLE `t_planet_buff_record` (
`id` varchar(64) NOT NULL COMMENT '主键',
`user_id` varchar(64) NOT NULL COMMENT '用户id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`buff_id` varchar(64) DEFAULT NULL COMMENT 'BUFF配置id',
`type` varchar(20) DEFAULT NULL COMMENT '类型',
`effect_value` decimal(10,2) DEFAULT '0.00' COMMENT '效果值',
`start_time` datetime DEFAULT NULL COMMENT '生效时间',
`end_time` datetime DEFAULT NULL COMMENT '失效时间',
`status` int DEFAULT '0' COMMENT '状态 0生效 1已过期',
`source` varchar(20) DEFAULT 'buy' COMMENT '来源 buy购买 box宝箱 hunt防护',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_status` (`user_id`,`status`),
KEY `idx_end_time` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-用户BUFF记录';
-- 8. 追捕记录表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_hunt_record`;
CREATE TABLE `t_planet_hunt_record` (
`id` varchar(64) NOT NULL COMMENT '主键',
`from_user_id` varchar(64) DEFAULT NULL COMMENT '发起人id',
`from_user_name` varchar(100) DEFAULT NULL COMMENT '发起人昵称',
`to_user_id` varchar(64) DEFAULT NULL COMMENT '目标用户id',
`to_user_name` varchar(100) DEFAULT NULL COMMENT '目标用户昵称',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`result` varchar(20) DEFAULT NULL COMMENT '结果 success/shield/fail',
`gain_tickets` int DEFAULT '0' COMMENT '缴获券',
`bounty_tickets` int DEFAULT '0' COMMENT '悬赏额外券',
`hunt_date` date DEFAULT NULL COMMENT '追捕日期',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_from_date` (`from_user_id`,`hunt_date`),
KEY `idx_to_date` (`to_user_id`,`hunt_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-追捕记录';
-- 9. 开奖记录表 -------------------------------------------------
DROP TABLE IF EXISTS `t_planet_draw_record`;
CREATE TABLE `t_planet_draw_record` (
`id` varchar(64) NOT NULL COMMENT '主键',
`pool_id` varchar(64) DEFAULT NULL COMMENT '奖池id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`period_no` varchar(64) DEFAULT NULL COMMENT '期号',
`pool_amount` decimal(10,2) DEFAULT '0.00' COMMENT '奖池金额',
`join_count` int DEFAULT '0' COMMENT '参与人数',
`ticket_total` int DEFAULT '0' COMMENT '券总数',
`winner_count` int DEFAULT '0' COMMENT '中奖人数',
`status` int DEFAULT '1' COMMENT '状态 1已开奖',
`draw_time` datetime DEFAULT NULL COMMENT '开奖时间',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_pool` (`pool_id`),
KEY `idx_region_period` (`region_id`,`period_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-开奖记录';
-- 10. 中奖记录表 ------------------------------------------------
DROP TABLE IF EXISTS `t_planet_draw_winner`;
CREATE TABLE `t_planet_draw_winner` (
`id` varchar(64) NOT NULL COMMENT '主键',
`draw_id` varchar(64) DEFAULT NULL COMMENT '开奖记录id',
`pool_id` varchar(64) DEFAULT NULL COMMENT '奖池id',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`period_no` varchar(64) DEFAULT NULL COMMENT '期号',
`user_id` varchar(64) DEFAULT NULL COMMENT '中奖用户id',
`user_name` varchar(100) DEFAULT NULL COMMENT '中奖用户昵称',
`reward_level` int DEFAULT NULL COMMENT '奖项层级',
`level_name` varchar(50) DEFAULT NULL COMMENT '奖项名称',
`reward_type` int DEFAULT '0' COMMENT '奖励类型 0现金 1优惠券',
`amount` decimal(10,2) DEFAULT '0.00' COMMENT '中奖金额',
`coupon_id` varchar(64) DEFAULT NULL COMMENT '优惠券id',
`is_received` int DEFAULT '0' COMMENT '是否已领取 0否 1是',
`received_time` datetime DEFAULT NULL COMMENT '领取时间',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_draw` (`draw_id`),
KEY `idx_region_period` (`region_id`,`period_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-中奖记录';
-- 11. 星球快讯表 ------------------------------------------------
DROP TABLE IF EXISTS `t_planet_news`;
CREATE TABLE `t_planet_news` (
`id` varchar(64) NOT NULL COMMENT '主键',
`region_id` varchar(64) DEFAULT NULL COMMENT '商圈id',
`type` varchar(20) DEFAULT NULL COMMENT '类型 rank/hunt/box/draw/sys',
`content` varchar(255) DEFAULT NULL COMMENT '快讯内容',
`user_id` varchar(64) DEFAULT NULL COMMENT '关联用户id',
`user_name` varchar(100) DEFAULT NULL COMMENT '关联用户昵称',
`is_top` int DEFAULT '0' COMMENT '是否置顶 0否 1是',
`enabled` int DEFAULT '1' COMMENT '是否展示 1是 0否',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_region_time` (`region_id`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT='白嫖星球-星球快讯';
-- =============================================================
-- 初始化全局默认配置(region_id 为空字符串表示全局默认)
-- =============================================================
-- 默认任务
INSERT INTO `t_planet_task` (`id`,`region_id`,`code`,`name`,`description`,`reward_tickets`,`daily_limit`,`sort`,`enabled`,`create_time`) VALUES
('ptk_waimai','','waimai','完成外卖订单','每完成一笔外卖订单 +1 星球券',1,0,1,1,NOW()),
('ptk_group','','group','完成团购订单','每完成一笔团购订单 +1 星球券',1,0,2,1,NOW()),
('ptk_invite','','invite','邀请好友注册','每邀请一位好友注册 +3 星球券',3,0,3,1,NOW()),
('ptk_sign','','sign','每日签到','每日签到领取 +1 星球券',1,1,4,1,NOW());
-- 默认BUFF
INSERT INTO `t_planet_buff` (`id`,`region_id`,`type`,`name`,`description`,`cost_tickets`,`duration_hours`,`effect_value`,`sort`,`enabled`,`create_time`) VALUES
('pbf_double','','double','双倍能量','24小时内获得星球券翻倍',5,24,2.00,1,1,NOW()),
('pbf_shield','','shield','星际防护罩','24小时内免疫追捕(养只小松鼠警卫)',8,24,0.00,2,1,NOW()),
('pbf_lucky','','lucky','幸运星辰','开奖权重 +20%',6,24,0.20,3,1,NOW()),
('pbf_hunt','','hunt','追猎雷达','追捕成功率提升',4,24,0.30,4,1,NOW()),
('pbf_stealth','','stealth','隐身斗篷','不出现在通缉榜',6,24,0.00,5,1,NOW());
Loading…
Cancel
Save