UserTeamShareTask.java 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package modules.user;
  2. import com.jfinal.plugin.activerecord.Db;
  3. import com.jfinal.plugin.activerecord.Record;
  4. import common.jfinal.AppConfig;
  5. import common.model.DepositLog;
  6. import common.model.Nftt;
  7. import common.model.Order;
  8. import common.model.User;
  9. import java.math.BigDecimal;
  10. import java.math.RoundingMode;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. import java.util.Map;
  14. import java.util.Set;
  15. import java.util.stream.Collectors;
  16. public class UserTeamShareTask implements Runnable {
  17. Order order;
  18. UserService userService;
  19. public UserTeamShareTask(Order order, UserService userService) {
  20. this.userService = userService;
  21. this.order = order;
  22. }
  23. // 统一分润逻辑,并进行容错和类型转换 (BigDecimal)
  24. private void processShare(String mobileNumber, BigDecimal percentage, String description) throws Exception {
  25. // 确保 order.getTotalPrice() 返回的是 BigDecimal,或者转换为 BigDecimal
  26. BigDecimal totalPrice = order.getBigDecimal("total_price");
  27. if (totalPrice == null) {
  28. throw new RuntimeException("订单总价为空,无法分润");
  29. }
  30. // 计算分润金额
  31. // 使用 BigDecimal 进行精确计算,避免浮点数精度问题
  32. BigDecimal shareAmount = totalPrice.multiply(percentage).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
  33. // 查找用户 (userService 是注入的)
  34. User user = userService.findUserByMobileNumber(mobileNumber);
  35. if (user == null) {
  36. AppConfig.LOGGER.warn("分润用户 {} 不存在,无法分润", mobileNumber);
  37. // 根据业务需求抛出异常或跳过
  38. return;
  39. }
  40. // 更新用户余额
  41. BigDecimal currentBalance = user.getBigDecimal("balance");
  42. // 如果 balance 字段为 null,初始化为 0
  43. if (currentBalance == null) {
  44. currentBalance = BigDecimal.ZERO;
  45. }
  46. user.set("balance", currentBalance.add(shareAmount));
  47. if (!user.update()) {
  48. throw new RuntimeException("更新用户 " + user.getStr("nickname") + " 余额失败");
  49. }
  50. // 记录分润日志
  51. DepositLog log = new DepositLog();
  52. log.set("create_time", System.currentTimeMillis());
  53. log.set("is_deleted", 0);
  54. log.set("description", description + " " + percentage.intValue() + "%"); // 描述中带上百分比
  55. log.set("amount", shareAmount);
  56. log.set("user_id", user.getLong("id"));
  57. if (!log.save()) {
  58. throw new RuntimeException("保存分润日志失败 for user " + user.getStr("nickname"));
  59. }
  60. AppConfig.LOGGER.info("{} {}: {}", description, user.getStr("nickname"), shareAmount);
  61. }
  62. /**
  63. * 第一类、单独账号分润
  64. * 0. 大领导 2%
  65. * 1. 团队奖励 5%
  66. * 2. 市场预留 3%
  67. * 4. 宣传外联 4%
  68. * 4. 技术公司运维 1%
  69. * 5. 平台费用 1%
  70. */
  71. public void class1() throws Exception {
  72. AppConfig.LOGGER.info("正在处理订单 {} 的第一类分润", order.getLong("id"));
  73. // 确保这些手机号是常量,或者从配置中读取
  74. processShare("27788881000", BigDecimal.valueOf(2), "大领导分润");
  75. processShare("27788881001", BigDecimal.valueOf(5), "团队奖励");
  76. processShare("27788881002", BigDecimal.valueOf(3), "市场预留");
  77. processShare("27788881003", BigDecimal.valueOf(4), "宣传外联");
  78. // 注意这里是 1%,用 BigDecimal.valueOf(1)
  79. processShare("27788881004", BigDecimal.valueOf(1), "技术公司运维");
  80. processShare("27788881005", BigDecimal.valueOf(1), "平台费用");
  81. AppConfig.LOGGER.info("订单 {} 第一类分润处理完成", order.getLong("id"));
  82. }
  83. /**
  84. * 第二类、直推分润 10%
  85. */
  86. public void class2() {
  87. User user = User.dao.findById(order.getUserId());
  88. User referrerUser = User.dao.findById(user.getReferrerId());
  89. // 直推人不存在,直接跳过
  90. if (referrerUser == null) {
  91. return;
  92. }
  93. // 确保 order.getTotalPrice() 返回的是 BigDecimal,或者转换为 BigDecimal
  94. BigDecimal totalPrice = order.getBigDecimal("total_price");
  95. BigDecimal percentage = BigDecimal.valueOf(10);
  96. if (totalPrice == null) {
  97. throw new RuntimeException("订单总价为空,无法分润");
  98. }
  99. // 直推10%分润
  100. BigDecimal shareAmount = totalPrice.multiply(percentage).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
  101. // 更新用户余额
  102. BigDecimal currentBalance = referrerUser.getBigDecimal("balance");
  103. // 如果 balance 字段为 null,初始化为 0
  104. if (currentBalance == null) {
  105. currentBalance = BigDecimal.ZERO;
  106. }
  107. referrerUser.set("balance", currentBalance.add(shareAmount));
  108. if (!referrerUser.update()) {
  109. throw new RuntimeException("更新用户 " + referrerUser.getMobileNumber() + " 余额失败");
  110. }
  111. // 记录分润日志
  112. String description = "直推分润";
  113. DepositLog log = new DepositLog();
  114. log.set("create_time", System.currentTimeMillis());
  115. log.set("is_deleted", 0);
  116. log.set("description", description + " " + percentage.intValue() + "%"); // 描述中带上百分比
  117. log.set("amount", shareAmount);
  118. log.set("user_id", referrerUser.getLong("id"));
  119. if (!log.save()) {
  120. throw new RuntimeException("保存分润日志失败 for user " + referrerUser.getMobileNumber());
  121. }
  122. AppConfig.LOGGER.info("{} {}: {}", description, referrerUser.getMobileNumber(), shareAmount);
  123. }
  124. /**
  125. * 第三类、NFT持有者分润
  126. * 1. 10%的部分
  127. * 2. 4%的部分
  128. */
  129. public void class3() {
  130. String queryUserIdSql = "SELECT user_id, COUNT(id) AS valid_order_count " +
  131. "FROM t_order " +
  132. "WHERE order_status = 60 AND delivery_status = 1 " +
  133. "GROUP BY user_id HAVING COUNT(id) > 0";
  134. List<Record> userOrderCounts = Db.find(queryUserIdSql);
  135. // 提取所有唯一的 user_id,用于下一步批量查询 User 对象
  136. Set<Long> eligibleUserIds = userOrderCounts.stream()
  137. .map(record -> record.getLong("user_id"))
  138. .collect(Collectors.toSet());
  139. // 如果没有符合条件的用户,直接返回
  140. if (eligibleUserIds.isEmpty()) {
  141. AppConfig.LOGGER.info("没有符合分润条件的用户。");
  142. return;
  143. }
  144. // 第二步:根据这些 user_id,批量查询出对应的 User 对象
  145. // JFinal 的 IN 操作比较方便,但是当列表过大时,分批查询 User 仍然是最佳实践
  146. // 假设 eligibleUserIds 不会过大,可以直接查询
  147. String inSql = eligibleUserIds.stream()
  148. .map(String::valueOf)
  149. .collect(Collectors.joining(","));
  150. AppConfig.LOGGER.info("开始查询用户");
  151. List<User> userList = User.dao.find("SELECT id, balance FROM t_user WHERE id IN (" + inSql + ")");
  152. AppConfig.LOGGER.info("用户查询完毕");
  153. // 为了方便处理,将 list 转换为 map,userOrderCounts 也转换为 map
  154. Map<Long, User> userMap = userList.stream()
  155. .collect(Collectors.toMap(User::getId, user -> user));
  156. Map<Long, Integer> userValidOrderCountMap = userOrderCounts.stream()
  157. .collect(Collectors.toMap(
  158. record -> record.getLong("user_id"),
  159. record -> record.getLong("valid_order_count").intValue()
  160. ));
  161. // 现在,你可以遍历 userOrderCounts,结合 userMap 进行分润处理
  162. List<User> usersToUpdate = new ArrayList<>();
  163. List<DepositLog> depositLogsToSave = new ArrayList<>();
  164. // 假设分润规则是:每个用户获得的总利润 = (该用户的有效订单数量 / 所有用户的总有效订单数量) * 总利润
  165. // 或者:简单地每个用户的固定份额
  166. // 这里我们用总订单数量来计算每个用户的分润比例
  167. long totalValidOrders = userValidOrderCountMap.values().stream().mapToLong(Integer::longValue).sum();
  168. // 避免除以零
  169. if (totalValidOrders == 0) {
  170. AppConfig.LOGGER.warn("所有符合条件用户的总订单数为0,无法按订单比例分润。");
  171. return;
  172. }
  173. // 总价值
  174. BigDecimal totalPrice = BigDecimal.valueOf(order.getTotalPrice());
  175. Nftt nftt = Nftt.dao.findById(10000);
  176. for (Record record : userOrderCounts) {
  177. // 参与分润的用户
  178. Long userId = record.getLong("user_id");
  179. // 当前用户的有效订单数量
  180. int validOrderCount = record.getLong("valid_order_count").intValue();
  181. User user = userMap.get(userId);
  182. if (user == null) {
  183. AppConfig.LOGGER.warn("未找到用户ID为 {} 的User对象,跳过分润。", userId);
  184. continue;
  185. }
  186. // 分润1,其中4%作为NFT持有者分享(0.04 * validOrderCount / totalValidOrders)
  187. BigDecimal shareArtio1 = BigDecimal.valueOf(0.04);
  188. BigDecimal userShareRatio1 = shareArtio1.multiply(BigDecimal.valueOf(validOrderCount))
  189. .divide(BigDecimal.valueOf(totalValidOrders), 8, RoundingMode.HALF_UP);// 确保高精度
  190. BigDecimal shareAmount1 = totalPrice.multiply(userShareRatio1).setScale(2, RoundingMode.HALF_UP); // 四舍五入到两位小数
  191. // 分润2,NFT持有者共享总数的10%,还没售卖出去的部分归平台所有 (0.1 * validOrderCount / Nftt.maxQuantity)
  192. BigDecimal shareArtio2 = BigDecimal.valueOf(0.1);
  193. BigDecimal userShareRatio2 = shareArtio2.multiply(BigDecimal.valueOf(validOrderCount))
  194. .divide(BigDecimal.valueOf(nftt.getMaxQuantity()), 8, RoundingMode.HALF_UP);// 确保高精度,注意这里的底数是MaxQuantity
  195. BigDecimal shareAmount2 = totalPrice.multiply(userShareRatio2).setScale(2, RoundingMode.HALF_UP); // 四舍五入到两位小数
  196. // 更新用户余额
  197. BigDecimal currentBalance = user.getBigDecimal("balance");
  198. if (currentBalance == null) {
  199. currentBalance = BigDecimal.ZERO;
  200. }
  201. user.set("balance", currentBalance.add(shareAmount1).add(shareAmount2).floatValue());
  202. usersToUpdate.add(user);
  203. // 创建分润日志
  204. DepositLog log1 = new DepositLog();
  205. log1.set("create_time", System.currentTimeMillis());
  206. log1.set("is_deleted", 0);
  207. log1.set("description", "4%作为NFT持有者分享," + validOrderCount + " / " + totalValidOrders); // 加入订单数量信息
  208. log1.set("amount", shareAmount1);
  209. log1.set("user_id", userId);
  210. depositLogsToSave.add(log1);
  211. DepositLog log2 = new DepositLog();
  212. log2.set("create_time", System.currentTimeMillis());
  213. log2.set("is_deleted", 0);
  214. log2.set("description", "NFT持有者共享总数的10%," + validOrderCount + " / " + nftt.getMaxQuantity()); // 加入订单数量信息
  215. log2.set("amount", shareAmount1);
  216. log2.set("user_id", userId);
  217. depositLogsToSave.add(log2);
  218. }
  219. // 在一个事务中执行所有批量更新和插入
  220. Db.tx(() -> {
  221. int[] userUpdateResult = Db.batchUpdate(usersToUpdate, 1000); // 批量更新用户
  222. int[] logSaveResult = Db.batchSave(depositLogsToSave, 1000); // 批量保存日志
  223. AppConfig.LOGGER.info("已批量更新 {} 个用户余额,批量保存 {} 条分润日志。", userUpdateResult.length, logSaveResult.length);
  224. return true; // 事务成功
  225. });
  226. }
  227. /**
  228. * 第四类、持有者激励分润,这个最复杂最后做,可能需要为user加一个时间戳字段
  229. */
  230. public void class4() {}
  231. @Override
  232. public void run() {
  233. // 确保整个分润过程在一个数据库事务中
  234. // 因为操作涉及到多个 User 和 DepositLog 的更新和插入
  235. Db.tx(() -> {
  236. try {
  237. this.class1();
  238. this.class2();
  239. this.class3();
  240. this.class4();
  241. return true; // 事务成功
  242. } catch (Exception e) {
  243. AppConfig.LOGGER.error("订单分润处理异常,订单ID: {}, 错误: {}", order.getLong("id"), e.getMessage(), e);
  244. return false; // 事务失败,回滚
  245. }
  246. });
  247. }
  248. }