Browse Source

对接拼团数据1

master
wangfukang 2 weeks ago
parent
commit
4ae8f4d271
  1. 15
      hiver-admin/src/main/resources/application.yml
  2. 16
      hiver-admin/test-output/test-report.html
  3. 30
      hiver-core/src/main/java/cc/hiver/core/entity/User.java
  4. 6
      hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java
  5. 11
      hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java
  6. 121
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java
  7. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java
  8. 57
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java
  9. 69
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java
  10. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java
  11. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java
  12. 42
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java
  13. 33
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java
  14. 43
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java
  15. 275
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java
  16. 34
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java
  17. 380
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java
  18. 44
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java
  19. 5
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java
  20. 48
      hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql
  21. 35
      hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java

15
hiver-admin/src/main/resources/application.yml

@ -589,4 +589,17 @@ wechatpay:
merchantSerialNo: 4B42A5EDF9CA2FC2ACEB21271A773A4F15B432A4 merchantSerialNo: 4B42A5EDF9CA2FC2ACEB21271A773A4F15B432A4
privateKeyPath: classpath:apiclient_key.pem privateKeyPath: classpath:apiclient_key.pem
platformCertificatePath: classpath:apiclient_cert.pem platformCertificatePath: classpath:apiclient_cert.pem
notifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/notify notifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/notify
transferUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills
transferSceneId: 1000
transferRemark: 邀请好友注册奖励
transferUserRecvPerception: 现金奖励
transferAuthorizationUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization
transferAuthorizationQueryUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no
transferAuthorizedTransferUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer
transferAuthorizationNotifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/transfer-auth-notify
transferSceneReportInfoType1: 活动名称
transferSceneReportInfoContent1: 邀请好友注册奖励
transferSceneReportInfoType2: 奖励说明
transferSceneReportInfoContent2: 好友完成注册并绑定手机号奖励
transferNotifyUrl:

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

@ -35,7 +35,7 @@
<a href="#"><span class="badge badge-primary">Hiver</span></a> <a href="#"><span class="badge badge-primary">Hiver</span></a>
</li> </li>
<li class="m-r-10"> <li class="m-r-10">
<a href="#"><span class="badge badge-primary">十月 28, 2026 10:46:04</span></a> <a href="#"><span class="badge badge-primary">十月 29, 2026 13:09:25</span></a>
</li> </li>
</ul> </ul>
</div> </div>
@ -84,7 +84,7 @@
<div class="test-detail"> <div class="test-detail">
<span class="meta text-white badge badge-sm"></span> <span class="meta text-white badge badge-sm"></span>
<p class="name">passTest</p> <p class="name">passTest</p>
<p class="text-sm"><span>10:46:04 上午</span> / <span>0.022 secs</span></p> <p class="text-sm"><span>13:09:26 下午</span> / <span>0.023 secs</span></p>
</div> </div>
<div class="test-contents d-none"> <div class="test-contents d-none">
<div class="detail-head"> <div class="detail-head">
@ -92,9 +92,9 @@
<div class="info"> <div class="info">
<div class='float-right'><span class='badge badge-default'>#test-id=1</span></div> <div class='float-right'><span class='badge badge-default'>#test-id=1</span></div>
<h5 class="test-status text-pass">passTest</h5> <h5 class="test-status text-pass">passTest</h5>
<span class='badge badge-success'>10.28.2026 10:46:04</span> <span class='badge badge-success'>10.29.2026 13:09:26</span>
<span class='badge badge-danger'>10.28.2026 10:46:04</span> <span class='badge badge-danger'>10.29.2026 13:09:26</span>
<span class='badge badge-default'>0.022 secs</span> <span class='badge badge-default'>0.023 secs</span>
</div> </div>
<div class="m-t-10 m-l-5"></div> <div class="m-t-10 m-l-5"></div>
</div> </div>
@ -104,7 +104,7 @@
<tbody> <tbody>
<tr class="event-row"> <tr class="event-row">
<td><span class="badge log pass-bg">Pass</span></td> <td><span class="badge log pass-bg">Pass</span></td>
<td>10:46:04</td> <td>13:09:26</td>
<td> <td>
Test passed Test passed
</td> </td>
@ -128,13 +128,13 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="card"><div class="card-body"> <div class="card"><div class="card-body">
<p class="m-b-0">Started</p> <p class="m-b-0">Started</p>
<h3>十月 28, 2026 10:46:04</h3> <h3>十月 29, 2026 13:09:25</h3>
</div></div> </div></div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card"><div class="card-body"> <div class="card"><div class="card-body">
<p class="m-b-0">Ended</p> <p class="m-b-0">Ended</p>
<h3>十月 28, 2026 10:46:05</h3> <h3>十月 29, 2026 13:09:26</h3>
</div></div> </div></div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">

30
hiver-core/src/main/java/cc/hiver/core/entity/User.java

@ -169,6 +169,36 @@ public class User extends HiverBaseEntity {
@ApiModelProperty(value = "邀请人Id") @ApiModelProperty(value = "邀请人Id")
private String inviterId; private String inviterId;
@ApiModelProperty(value = "拉新来源区域id")
private String inviteRegionId;
@ApiModelProperty(value = "拉新设备标识")
private String inviteDeviceId;
@ApiModelProperty(value = "是否邀请链路新用户 1是 0否")
private Integer inviteNewUser = 0;
@ApiModelProperty(value = "拉新奖励状态 0未处理 1成功 2失败 3跳过")
private Integer inviteRewardStatus = 0;
@ApiModelProperty(value = "拉新奖励金额")
private BigDecimal inviteRewardAmount = new BigDecimal(0);
@ApiModelProperty(value = "拉新奖励时间")
private Date inviteRewardTime;
@ApiModelProperty(value = "拉新转账免确认授权商户单号")
private String inviteTransferAuthNo;
@ApiModelProperty(value = "拉新转账微信免确认授权单号")
private String inviteTransferAuthId;
@ApiModelProperty(value = "拉新转账免确认授权状态")
private String inviteTransferAuthState;
@ApiModelProperty(value = "拉新转账免确认授权时间")
private Date inviteTransferAuthTime;
@ApiModelProperty("当前账户余额") @ApiModelProperty("当前账户余额")
private BigDecimal depoBal = new BigDecimal(0); private BigDecimal depoBal = new BigDecimal(0);

6
hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java

@ -17,6 +17,12 @@ public class WechatVo {
@ApiModelProperty(value = "inviterId") @ApiModelProperty(value = "inviterId")
private String inviterId; private String inviterId;
@ApiModelProperty(value = "拉新来源区域id")
private String regionId;
@ApiModelProperty(value = "拉新设备标识")
private String deviceId;
@ApiModelProperty(value = "wechatName") @ApiModelProperty(value = "wechatName")
private String wechatName; private String wechatName;

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

@ -690,6 +690,7 @@ public class UserController {
} }
if (wechatUser == null) { if (wechatUser == null) {
boolean newUserCreated = false;
log.info("微信登录未根据userid和unionid查询到用户,准备新增用户userId:{};unionid:{}",wechatVo.getUserId(),unionid); log.info("微信登录未根据userid和unionid查询到用户,准备新增用户userId:{};unionid:{}",wechatVo.getUserId(),unionid);
try { try {
// 新增用户信息 // 新增用户信息
@ -704,10 +705,15 @@ public class UserController {
wechatUser.setPassword(encryptPass); wechatUser.setPassword(encryptPass);
wechatUser.setType(UserConstant.USER_TYPE_NORMAL); wechatUser.setType(UserConstant.USER_TYPE_NORMAL);
//设置邀请人 //设置邀请人
if(wechatVo.getInviterId() != null){ if(StringUtils.isNotEmpty(wechatVo.getInviterId())){
wechatUser.setInviterId(wechatVo.getInviterId()); wechatUser.setInviterId(wechatVo.getInviterId());
wechatUser.setInviteRegionId(wechatVo.getRegionId());
wechatUser.setInviteDeviceId(wechatVo.getDeviceId());
wechatUser.setInviteNewUser(1);
wechatUser.setInviteRewardStatus(0);
} }
wechatUser = userService.save(wechatUser); wechatUser = userService.save(wechatUser);
newUserCreated = true;
// 设置用户角色 // 设置用户角色
// 默认角色 // 默认角色
final List<Role> roleList = roleService.findByDefaultRole(true); final List<Role> roleList = roleService.findByDefaultRole(true);
@ -725,10 +731,13 @@ public class UserController {
//customerService.updateUserId(wechatVo.getCustomId(), wechatUser.getId()); //customerService.updateUserId(wechatVo.getCustomId(), wechatUser.getId());
// 返回当前登录人的信息 // 返回当前登录人的信息
resultMap.put("msg", "新增用户信息,并绑定成功!"); resultMap.put("msg", "新增用户信息,并绑定成功!");
resultMap.put("isNewUser", newUserCreated);
//resultMap.put("customId", wechatVo.getCustomId()); //resultMap.put("customId", wechatVo.getCustomId());
} catch (Exception e) { } catch (Exception e) {
return ResultUtil.error(e.getMessage()); return ResultUtil.error(e.getMessage());
} }
} else {
resultMap.put("isNewUser", false);
} }
wechatUser.setUnionid(unionid); wechatUser.setUnionid(unionid);
wechatUser.setWechatName(wechatVo.getWechatName()); wechatUser.setWechatName(wechatVo.getWechatName());

121
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java

@ -0,0 +1,121 @@
package cc.hiver.mall.controller;
import cc.hiver.core.common.utils.ResultUtil;
import cc.hiver.core.common.utils.SecurityUtil;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.core.common.vo.Result;
import cc.hiver.core.entity.User;
import cc.hiver.mall.invite.entity.InviteRewardConfig;
import cc.hiver.mall.invite.entity.InviteRewardRecord;
import cc.hiver.mall.invite.service.InviteRewardService;
import cc.hiver.mall.invite.service.WechatTransferAuthorizationResult;
import cc.hiver.mall.utils.WechatPayConfig;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@Api(tags = "拉新返现接口")
@RequestMapping("/hiver/app/inviteReward")
public class InviteRewardController {
@Autowired
private SecurityUtil securityUtil;
@Autowired
private InviteRewardService inviteRewardService;
@Autowired
private WechatPayConfig wechatPayConfig;
@Value("${wechatpay.mchId:}")
private String mchId;
private PageVo buildPage(Integer pageNumber, Integer pageSize) {
PageVo pageVo = new PageVo();
pageVo.setPageNumber(pageNumber == null ? 1 : pageNumber);
pageVo.setPageSize(pageSize == null ? 10 : pageSize);
return pageVo;
}
@ApiOperation(value = "我的拉新返现记录")
@RequestMapping(value = "/my/page", method = RequestMethod.GET)
public Result<Page<InviteRewardRecord>> myPage(@RequestParam(required = false) Integer pageNumber,
@RequestParam(required = false) Integer pageSize) {
User user = securityUtil.getCurrUser();
if (user == null || user.getId() == null) {
return ResultUtil.error("请先登录");
}
return new ResultUtil<Page<InviteRewardRecord>>()
.setData(inviteRewardService.pageRecords(buildPage(pageNumber, pageSize), null, user.getId()));
}
@ApiOperation(value = "当前区域拉新返现配置")
@RequestMapping(value = "/config", method = RequestMethod.GET)
public Result<InviteRewardConfig> config(@RequestParam(required = false) String regionId) {
return new ResultUtil<InviteRewardConfig>().setData(inviteRewardService.getEnabledConfig(regionId));
}
@ApiOperation(value = "免确认收款授权状态")
@RequestMapping(value = "/transferAuth/status", method = RequestMethod.GET)
public Result<Map<String, Object>> transferAuthStatus() {
User user = securityUtil.getCurrUser();
if (user == null || user.getId() == null) {
return ResultUtil.error("请先登录");
}
WechatTransferAuthorizationResult result = inviteRewardService.refreshTransferAuthorization(user, false);
if (result.isSuccess()) {
return new ResultUtil<Map<String, Object>>().setData(buildAuthData(user, result));
}
Map<String, Object> data = buildAuthData(user, null);
return new ResultUtil<Map<String, Object>>().setData(data);
}
@ApiOperation(value = "申请免确认收款授权")
@RequestMapping(value = "/transferAuth/apply", method = RequestMethod.POST)
public Result<Map<String, Object>> applyTransferAuth() {
User user = securityUtil.getCurrUser();
if (user == null || user.getId() == null) {
return ResultUtil.error("请先登录");
}
WechatTransferAuthorizationResult result = inviteRewardService.applyTransferAuthorization(user);
if (!result.isSuccess()) {
return ResultUtil.error(result.getMessage());
}
return new ResultUtil<Map<String, Object>>().setData(buildAuthData(user, result));
}
@ApiOperation(value = "刷新免确认收款授权状态")
@RequestMapping(value = "/transferAuth/query", method = RequestMethod.POST)
public Result<Map<String, Object>> queryTransferAuth() {
User user = securityUtil.getCurrUser();
if (user == null || user.getId() == null) {
return ResultUtil.error("请先登录");
}
WechatTransferAuthorizationResult result = inviteRewardService.refreshTransferAuthorization(user, true);
if (!result.isSuccess()) {
return ResultUtil.error(result.getMessage());
}
return new ResultUtil<Map<String, Object>>().setData(buildAuthData(user, result));
}
private Map<String, Object> buildAuthData(User user, WechatTransferAuthorizationResult result) {
Map<String, Object> data = new HashMap<>();
data.put("mchId", mchId);
data.put("appId", wechatPayConfig.getAppId());
data.put("authNo", result == null ? user.getInviteTransferAuthNo() : result.getOutAuthorizationNo());
data.put("authId", result == null ? user.getInviteTransferAuthId() : result.getAuthorizationId());
data.put("state", result == null ? user.getInviteTransferAuthState() : result.getState());
data.put("packageInfo", result == null ? "" : result.getPackageInfo());
return data;
}
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java

@ -209,6 +209,15 @@ public class WechatPayController {
} }
} }
@PostMapping("/transfer-auth-notify")
public ResponseEntity<Map<String, String>> transferAuthNotify(@RequestBody Map<String, Object> request) {
log.info("接收到微信商家转账免确认授权回调:{}", request);
Map<String, String> result = new HashMap<>();
result.put("code", "SUCCESS");
result.put("message", "成功");
return ResponseEntity.ok(result);
}
public Map<String, String> buildPaySign(String prepayId) throws Exception { public Map<String, String> buildPaySign(String prepayId) throws Exception {
// 1. 生成参数 // 1. 生成参数
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 秒 String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 秒

57
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java

@ -0,0 +1,57 @@
package cc.hiver.mall.invite.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;
@Data
@ApiModel(value = "拉新返现配置")
@TableName(value = "t_invite_reward_config", autoResultMap = true)
public class InviteRewardConfig implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "区域id")
private String regionId;
@ApiModelProperty(value = "每个有效新用户奖励金额")
private BigDecimal rewardAmount;
@ApiModelProperty(value = "每日发放上限 0不限")
private Integer dailyLimit;
@ApiModelProperty(value = "总预算 0不限")
private BigDecimal totalBudget;
@ApiModelProperty(value = "已发放预算")
private BigDecimal usedBudget;
@ApiModelProperty(value = "是否启用 1是 0否")
private Integer enabled;
@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;
}

69
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java

@ -0,0 +1,69 @@
package cc.hiver.mall.invite.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;
@Data
@ApiModel(value = "拉新返现奖励记录")
@TableName(value = "t_invite_reward_record", autoResultMap = true)
public class InviteRewardRecord implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@TableId
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value = "邀请人用户id")
private String inviterId;
@ApiModelProperty(value = "被邀请人用户id")
private String invitedUserId;
@ApiModelProperty(value = "被邀请人手机号")
private String invitedMobile;
@ApiModelProperty(value = "邀请设备标识")
private String inviteDeviceId;
@ApiModelProperty(value = "发生奖励时的区域id")
private String regionId;
@ApiModelProperty(value = "奖励金额")
private BigDecimal amount;
@ApiModelProperty(value = "状态 0处理中 1成功 2失败 3跳过")
private Integer status;
@ApiModelProperty(value = "微信商家转账商户单号")
private String outBillNo;
@ApiModelProperty(value = "微信商家转账单号")
private String transferBillNo;
@ApiModelProperty(value = "失败或跳过原因")
private String failReason;
@ApiModelProperty(value = "微信响应")
private String wxResponse;
@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;
}

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java

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

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java

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

42
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java

@ -0,0 +1,42 @@
package cc.hiver.mall.invite.service;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class InviteRewardResult {
private boolean rewarded;
private boolean skipped;
private boolean processing;
private BigDecimal amount;
private String message;
public static InviteRewardResult rewarded(BigDecimal amount, String message) {
InviteRewardResult result = new InviteRewardResult();
result.setRewarded(true);
result.setAmount(amount);
result.setMessage(message);
return result;
}
public static InviteRewardResult skipped(String message) {
InviteRewardResult result = new InviteRewardResult();
result.setSkipped(true);
result.setMessage(message);
return result;
}
public static InviteRewardResult processing(String message) {
InviteRewardResult result = new InviteRewardResult();
result.setProcessing(true);
result.setMessage(message);
return result;
}
public static InviteRewardResult failed(String message) {
InviteRewardResult result = new InviteRewardResult();
result.setMessage(message);
return result;
}
}

33
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java

@ -0,0 +1,33 @@
package cc.hiver.mall.invite.service;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.core.entity.User;
import cc.hiver.mall.invite.entity.InviteRewardConfig;
import cc.hiver.mall.invite.entity.InviteRewardRecord;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
public interface InviteRewardService {
Page<InviteRewardConfig> pageConfigs(PageVo pageVo, String regionId);
Page<InviteRewardRecord> pageRecords(PageVo pageVo, String regionId, String inviterId);
InviteRewardConfig getEnabledConfig(String regionId);
InviteRewardConfig saveConfig(InviteRewardConfig config);
void deleteConfig(String id);
WechatTransferAuthorizationResult applyTransferAuthorization(User user);
WechatTransferAuthorizationResult refreshTransferAuthorization(User user, boolean displayAuthorization);
InviteRewardResult rewardAfterPhoneBound(User invitedUser,
String phoneNumber,
String inviterId,
String regionId,
String deviceId,
Double lat,
Double lng,
boolean firstPhoneBind);
}

43
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java

@ -0,0 +1,43 @@
package cc.hiver.mall.invite.service;
import lombok.Data;
@Data
public class WechatMerchantTransferResult {
private boolean success;
private boolean processing;
private String state;
private String transferBillNo;
private String packageInfo;
private String responseBody;
private String message;
public static WechatMerchantTransferResult success(String state, String transferBillNo, String packageInfo, String responseBody) {
WechatMerchantTransferResult result = new WechatMerchantTransferResult();
result.setSuccess(true);
result.setState(state);
result.setTransferBillNo(transferBillNo);
result.setPackageInfo(packageInfo);
result.setResponseBody(responseBody);
return result;
}
public static WechatMerchantTransferResult processing(String state, String transferBillNo, String packageInfo, String message, String responseBody) {
WechatMerchantTransferResult result = new WechatMerchantTransferResult();
result.setProcessing(true);
result.setState(state);
result.setTransferBillNo(transferBillNo);
result.setPackageInfo(packageInfo);
result.setMessage(message);
result.setResponseBody(responseBody);
return result;
}
public static WechatMerchantTransferResult fail(String message, String responseBody) {
WechatMerchantTransferResult result = new WechatMerchantTransferResult();
result.setSuccess(false);
result.setMessage(message);
result.setResponseBody(responseBody);
return result;
}
}

275
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java

@ -0,0 +1,275 @@
package cc.hiver.mall.invite.service;
import cc.hiver.mall.utils.WechatPayConfig;
import cc.hiver.mall.utils.WechatPaySigner;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class WechatMerchantTransferService {
@Value("${wechatpay.transferUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills}")
private String transferUrl;
@Value("${wechatpay.transferSceneId:1000}")
private String transferSceneId;
@Value("${wechatpay.transferRemark:邀请好友注册奖励}")
private String transferRemark;
@Value("${wechatpay.transferUserRecvPerception:现金奖励}")
private String userRecvPerception;
@Value("${wechatpay.transferNotifyUrl:}")
private String transferNotifyUrl;
@Value("${wechatpay.transferAuthorizationUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization}")
private String transferAuthorizationUrl;
@Value("${wechatpay.transferAuthorizationQueryUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no}")
private String transferAuthorizationQueryUrl;
@Value("${wechatpay.transferAuthorizedTransferUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer}")
private String transferAuthorizedTransferUrl;
@Value("${wechatpay.transferAuthorizationNotifyUrl:}")
private String transferAuthorizationNotifyUrl;
@Value("${wechatpay.transferSceneReportInfoType1:活动名称}")
private String transferSceneReportInfoType1;
@Value("${wechatpay.transferSceneReportInfoContent1:邀请好友注册奖励}")
private String transferSceneReportInfoContent1;
@Value("${wechatpay.transferSceneReportInfoType2:奖励说明}")
private String transferSceneReportInfoType2;
@Value("${wechatpay.transferSceneReportInfoContent2:好友完成注册并绑定手机号奖励}")
private String transferSceneReportInfoContent2;
@Autowired
private WechatPayConfig config;
@Autowired
private WechatPaySigner signer;
private final OkHttpClient httpClient = new OkHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
public WechatTransferAuthorizationResult applyAuthorization(String openid, String displayName, String outAuthorizationNo) {
if (StringUtils.isBlank(openid)) {
return WechatTransferAuthorizationResult.fail("用户缺少小程序openid", "");
}
if (StringUtils.isBlank(outAuthorizationNo)) {
return WechatTransferAuthorizationResult.fail("授权单号不能为空", "");
}
if (StringUtils.isBlank(transferAuthorizationNotifyUrl)) {
return WechatTransferAuthorizationResult.fail("未配置免确认收款授权回调地址", "");
}
try {
Map<String, Object> reqBody = new HashMap<>();
reqBody.put("out_authorization_no", outAuthorizationNo);
reqBody.put("appid", config.getAppId());
reqBody.put("openid", openid);
reqBody.put("transfer_scene_id", transferSceneId);
reqBody.put("user_display_name", StringUtils.defaultIfBlank(displayName, "用户"));
reqBody.put("user_recv_perception", userRecvPerception);
reqBody.put("authorization_notify_url", transferAuthorizationNotifyUrl);
String jsonBody = objectMapper.writeValueAsString(reqBody);
Request request = newJsonRequest(transferAuthorizationUrl, jsonBody);
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body() == null ? "" : response.body().string();
if (!response.isSuccessful()) {
return WechatTransferAuthorizationResult.fail("微信免确认收款授权申请失败: " + response.code(), responseBody);
}
JsonNode root = objectMapper.readTree(responseBody);
return WechatTransferAuthorizationResult.success(
root.path("out_authorization_no").asText(outAuthorizationNo),
root.path("authorization_id").asText(""),
root.path("state").asText(""),
root.path("package_info").asText(""),
responseBody
);
}
} catch (Exception e) {
return WechatTransferAuthorizationResult.fail(e.getMessage(), "");
}
}
public WechatTransferAuthorizationResult queryAuthorization(String outAuthorizationNo, boolean displayAuthorization) {
if (StringUtils.isBlank(outAuthorizationNo)) {
return WechatTransferAuthorizationResult.fail("授权单号不能为空", "");
}
try {
String url = transferAuthorizationQueryUrl + "/" + outAuthorizationNo
+ "?is_display_authorization=" + displayAuthorization;
Request request = new Request.Builder()
.url(url)
.get()
.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + signer.sign("GET", url, ""))
.addHeader("Accept", "application/json")
.addHeader("User-Agent", "Hiver/1.0")
.build();
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body() == null ? "" : response.body().string();
if (!response.isSuccessful()) {
return WechatTransferAuthorizationResult.fail("微信免确认收款授权查询失败: " + response.code(), responseBody);
}
JsonNode root = objectMapper.readTree(responseBody);
return WechatTransferAuthorizationResult.success(
root.path("out_authorization_no").asText(outAuthorizationNo),
root.path("authorization_id").asText(""),
root.path("state").asText(""),
root.path("package_info").asText(""),
responseBody
);
}
} catch (Exception e) {
return WechatTransferAuthorizationResult.fail(e.getMessage(), "");
}
}
public WechatMerchantTransferResult transferWithAuthorization(String authorizationId, String outAuthorizationNo,
BigDecimal amount, String outBillNo) {
if (StringUtils.isAllBlank(authorizationId, outAuthorizationNo)) {
return WechatMerchantTransferResult.fail("邀请人未开通免确认收款授权", "");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return WechatMerchantTransferResult.fail("转账金额必须大于0", "");
}
try {
Map<String, Object> reqBody = new HashMap<>();
reqBody.put("appid", config.getAppId());
reqBody.put("out_bill_no", outBillNo);
reqBody.put("transfer_scene_id", transferSceneId);
reqBody.put("transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue());
reqBody.put("transfer_remark", transferRemark);
reqBody.put("user_recv_perception", userRecvPerception);
if (StringUtils.isNotBlank(authorizationId)) {
reqBody.put("authorization_id", authorizationId);
} else {
reqBody.put("out_authorization_no", outAuthorizationNo);
}
List<Map<String, String>> reportInfos = buildSceneReportInfos();
if (!reportInfos.isEmpty()) {
reqBody.put("transfer_scene_report_infos", reportInfos);
}
String jsonBody = objectMapper.writeValueAsString(reqBody);
Request request = newJsonRequest(transferAuthorizedTransferUrl, jsonBody);
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body() == null ? "" : response.body().string();
if (!response.isSuccessful()) {
return WechatMerchantTransferResult.fail("微信商家授权转账失败: " + response.code(), responseBody);
}
JsonNode root = objectMapper.readTree(responseBody);
String transferBillNo = root.path("transfer_bill_no").asText("");
String state = root.path("state").asText("");
if ("SUCCESS".equals(state)) {
return WechatMerchantTransferResult.success(state, transferBillNo, "", responseBody);
}
return WechatMerchantTransferResult.fail("微信商家授权转账状态: " + state, responseBody);
}
} catch (Exception e) {
return WechatMerchantTransferResult.fail(e.getMessage(), "");
}
}
public WechatMerchantTransferResult transferToOpenid(String openid, BigDecimal amount, String outBillNo) {
if (StringUtils.isBlank(openid)) {
return WechatMerchantTransferResult.fail("邀请人缺少小程序openid", "");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return WechatMerchantTransferResult.fail("转账金额必须大于0", "");
}
try {
Map<String, Object> reqBody = new HashMap<>();
reqBody.put("appid", config.getAppId());
reqBody.put("out_bill_no", outBillNo);
reqBody.put("transfer_scene_id", transferSceneId);
reqBody.put("openid", openid);
reqBody.put("transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue());
reqBody.put("transfer_remark", transferRemark);
reqBody.put("user_recv_perception", userRecvPerception);
List<Map<String, String>> reportInfos = buildSceneReportInfos();
if (!reportInfos.isEmpty()) {
reqBody.put("transfer_scene_report_infos", reportInfos);
}
if (StringUtils.isNotBlank(transferNotifyUrl)) {
reqBody.put("notify_url", transferNotifyUrl);
}
String jsonBody = objectMapper.writeValueAsString(reqBody);
String authorization = signer.sign("POST", transferUrl, jsonBody);
Request request = new Request.Builder()
.url(transferUrl)
.post(RequestBody.create(MediaType.get("application/json"), jsonBody))
.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("User-Agent", "Hiver/1.0")
.build();
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body() == null ? "" : response.body().string();
if (!response.isSuccessful()) {
return WechatMerchantTransferResult.fail("微信商家转账失败: " + response.code(), responseBody);
}
JsonNode root = objectMapper.readTree(responseBody);
String transferBillNo = root.path("transfer_bill_no").asText("");
String state = root.path("state").asText("");
String packageInfo = root.path("package_info").asText("");
if ("SUCCESS".equals(state)) {
return WechatMerchantTransferResult.success(state, transferBillNo, packageInfo, responseBody);
}
if ("WAIT_USER_CONFIRM".equals(state)) {
return WechatMerchantTransferResult.processing(state, transferBillNo, packageInfo, "待邀请人确认收款", responseBody);
}
return WechatMerchantTransferResult.fail("微信商家转账状态: " + state, responseBody);
}
} catch (Exception e) {
return WechatMerchantTransferResult.fail(e.getMessage(), "");
}
}
private Request newJsonRequest(String url, String jsonBody) throws Exception {
String authorization = signer.sign("POST", url, jsonBody);
return new Request.Builder()
.url(url)
.post(RequestBody.create(MediaType.get("application/json"), jsonBody))
.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("User-Agent", "Hiver/1.0")
.build();
}
private List<Map<String, String>> buildSceneReportInfos() {
List<Map<String, String>> reportInfos = new ArrayList<>();
addSceneReportInfo(reportInfos, transferSceneReportInfoType1, transferSceneReportInfoContent1);
addSceneReportInfo(reportInfos, transferSceneReportInfoType2, transferSceneReportInfoContent2);
return reportInfos;
}
private void addSceneReportInfo(List<Map<String, String>> reportInfos, String infoType, String infoContent) {
if (StringUtils.isBlank(infoType) || StringUtils.isBlank(infoContent)) {
return;
}
Map<String, String> info = new HashMap<>();
info.put("info_type", infoType);
info.put("info_content", infoContent);
reportInfos.add(info);
}
}

34
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java

@ -0,0 +1,34 @@
package cc.hiver.mall.invite.service;
import lombok.Data;
@Data
public class WechatTransferAuthorizationResult {
private boolean success;
private String outAuthorizationNo;
private String authorizationId;
private String state;
private String packageInfo;
private String responseBody;
private String message;
public static WechatTransferAuthorizationResult success(String outAuthorizationNo, String authorizationId,
String state, String packageInfo, String responseBody) {
WechatTransferAuthorizationResult result = new WechatTransferAuthorizationResult();
result.setSuccess(true);
result.setOutAuthorizationNo(outAuthorizationNo);
result.setAuthorizationId(authorizationId);
result.setState(state);
result.setPackageInfo(packageInfo);
result.setResponseBody(responseBody);
return result;
}
public static WechatTransferAuthorizationResult fail(String message, String responseBody) {
WechatTransferAuthorizationResult result = new WechatTransferAuthorizationResult();
result.setSuccess(false);
result.setMessage(message);
result.setResponseBody(responseBody);
return result;
}
}

380
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java

@ -0,0 +1,380 @@
package cc.hiver.mall.invite.service.impl;
import cc.hiver.core.common.vo.PageVo;
import cc.hiver.core.entity.User;
import cc.hiver.core.service.UserService;
import cc.hiver.mall.entity.ShopArea;
import cc.hiver.mall.invite.entity.InviteRewardConfig;
import cc.hiver.mall.invite.entity.InviteRewardRecord;
import cc.hiver.mall.invite.mapper.InviteRewardConfigMapper;
import cc.hiver.mall.invite.mapper.InviteRewardRecordMapper;
import cc.hiver.mall.invite.service.*;
import cc.hiver.mall.service.ShopAreaService;
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 org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
@Service
public class InviteRewardServiceImpl implements InviteRewardService {
private static final int STATUS_PROCESSING = 0;
private static final int STATUS_SUCCESS = 1;
private static final int STATUS_FAILED = 2;
private static final int STATUS_SKIPPED = 3;
@Autowired
private InviteRewardConfigMapper configMapper;
@Autowired
private InviteRewardRecordMapper recordMapper;
@Autowired
private UserService userService;
@Autowired
private ShopAreaService shopAreaService;
@Autowired
private WechatMerchantTransferService transferService;
private static String genId() {
return UUID.randomUUID().toString().replace("-", "");
}
private <T> Page<T> page(PageVo pageVo) {
int num = pageVo == null || pageVo.getPageNumber() <= 0 ? 1 : pageVo.getPageNumber();
int size = pageVo == null || pageVo.getPageSize() <= 0 ? 10 : pageVo.getPageSize();
return new Page<>(num, size);
}
@Override
public Page<InviteRewardConfig> pageConfigs(PageVo pageVo, String regionId) {
LambdaQueryWrapper<InviteRewardConfig> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(regionId)) {
qw.eq(InviteRewardConfig::getRegionId, regionId);
}
qw.orderByDesc(InviteRewardConfig::getCreateTime);
return configMapper.selectPage(page(pageVo), qw);
}
@Override
public Page<InviteRewardRecord> pageRecords(PageVo pageVo, String regionId, String inviterId) {
LambdaQueryWrapper<InviteRewardRecord> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(regionId)) {
qw.eq(InviteRewardRecord::getRegionId, regionId);
}
if (StringUtils.isNotBlank(inviterId)) {
qw.eq(InviteRewardRecord::getInviterId, inviterId);
}
qw.orderByDesc(InviteRewardRecord::getCreateTime);
return recordMapper.selectPage(page(pageVo), qw);
}
@Override
public InviteRewardConfig saveConfig(InviteRewardConfig config) {
Date now = new Date();
if (config.getRewardAmount() == null) {
config.setRewardAmount(BigDecimal.ZERO);
}
if (config.getDailyLimit() == null) {
config.setDailyLimit(0);
}
if (config.getTotalBudget() == null) {
config.setTotalBudget(BigDecimal.ZERO);
}
if (config.getUsedBudget() == null) {
config.setUsedBudget(BigDecimal.ZERO);
}
if (config.getEnabled() == null) {
config.setEnabled(1);
}
config.setUpdateTime(now);
if (StringUtils.isBlank(config.getId())) {
config.setId(genId());
config.setCreateTime(now);
configMapper.insert(config);
} else {
configMapper.updateById(config);
}
return config;
}
@Override
public void deleteConfig(String id) {
configMapper.deleteById(id);
}
@Override
public WechatTransferAuthorizationResult applyTransferAuthorization(User user) {
if (user == null || StringUtils.isBlank(user.getId())) {
return WechatTransferAuthorizationResult.fail("用户不存在", "");
}
if (StringUtils.isBlank(user.getMiniProgramOpenid())) {
return WechatTransferAuthorizationResult.fail("用户缺少小程序openid", "");
}
String outAuthorizationNo = StringUtils.defaultIfBlank(user.getInviteTransferAuthNo(), buildAuthorizationNo(user));
WechatTransferAuthorizationResult queryResult = transferService.queryAuthorization(outAuthorizationNo, true);
if (queryResult.isSuccess()) {
saveAuthorizationResult(user, outAuthorizationNo, queryResult);
if ("TAKING_EFFECT".equals(queryResult.getState()) || StringUtils.isNotBlank(queryResult.getPackageInfo())) {
return queryResult;
}
} else if (!isAuthorizationNotFound(queryResult)) {
return queryResult;
}
WechatTransferAuthorizationResult result = transferService.applyAuthorization(
user.getMiniProgramOpenid(),
StringUtils.defaultIfBlank(user.getNickname(), StringUtils.defaultIfBlank(user.getUsername(), user.getMobile())),
outAuthorizationNo
);
if (result.isSuccess()) {
saveAuthorizationResult(user, outAuthorizationNo, result);
}
return result;
}
@Override
public WechatTransferAuthorizationResult refreshTransferAuthorization(User user, boolean displayAuthorization) {
if (user == null || StringUtils.isBlank(user.getId())) {
return WechatTransferAuthorizationResult.fail("用户不存在", "");
}
String outAuthorizationNo = StringUtils.defaultIfBlank(user.getInviteTransferAuthNo(), buildAuthorizationNo(user));
WechatTransferAuthorizationResult result = transferService.queryAuthorization(outAuthorizationNo, displayAuthorization);
if (result.isSuccess()) {
saveAuthorizationResult(user, outAuthorizationNo, result);
}
return result;
}
private void saveAuthorizationResult(User user, String outAuthorizationNo, WechatTransferAuthorizationResult result) {
Date now = new Date();
user.setInviteTransferAuthNo(outAuthorizationNo);
user.setInviteTransferAuthState(result.getState());
if (StringUtils.isNotBlank(result.getAuthorizationId())) {
user.setInviteTransferAuthId(result.getAuthorizationId());
}
if ("TAKING_EFFECT".equals(result.getState())) {
user.setInviteTransferAuthTime(now);
}
userService.update(user);
}
private boolean isAuthorizationNotFound(WechatTransferAuthorizationResult result) {
String message = StringUtils.defaultString(result == null ? "" : result.getMessage());
String body = StringUtils.defaultString(result == null ? "" : result.getResponseBody());
return message.contains("404")
|| body.contains("RESOURCE_NOT_EXISTS")
|| body.contains("NOT_FOUND")
|| body.contains("不存在");
}
@Override
@Transactional(rollbackFor = Exception.class)
public InviteRewardResult rewardAfterPhoneBound(User invitedUser, String phoneNumber, String inviterId, String regionId,
String deviceId, Double lat, Double lng, boolean firstPhoneBind) {
if (invitedUser == null || StringUtils.isBlank(invitedUser.getId())) {
return InviteRewardResult.skipped("用户不存在");
}
String finalInviterId = StringUtils.defaultIfBlank(invitedUser.getInviterId(), inviterId);
String finalRegionId = StringUtils.defaultIfBlank(invitedUser.getInviteRegionId(), regionId);
String finalDeviceId = StringUtils.defaultIfBlank(invitedUser.getInviteDeviceId(), deviceId);
if (!firstPhoneBind) {
return InviteRewardResult.skipped("非首次绑定手机号");
}
if (!Integer.valueOf(1).equals(invitedUser.getInviteNewUser())) {
return InviteRewardResult.skipped("非邀请链路新用户");
}
if (StringUtils.isBlank(finalInviterId) || StringUtils.isBlank(finalRegionId)) {
return InviteRewardResult.skipped("缺少邀请信息");
}
if (StringUtils.equals(finalInviterId, invitedUser.getId())) {
markUserSkipped(invitedUser, finalRegionId, finalDeviceId);
return InviteRewardResult.skipped("不能邀请自己");
}
User inviter = userService.findById(finalInviterId);
if (inviter == null || StringUtils.isBlank(inviter.getMiniProgramOpenid())) {
markUserSkipped(invitedUser, finalRegionId, finalDeviceId);
return InviteRewardResult.skipped("邀请人缺少微信收款身份");
}
if (!"TAKING_EFFECT".equals(inviter.getInviteTransferAuthState())
|| StringUtils.isAllBlank(inviter.getInviteTransferAuthId(), inviter.getInviteTransferAuthNo())) {
return InviteRewardResult.skipped("邀请人未开通免确认收款授权");
}
if (!isLocationInRegion(finalRegionId, lat, lng)) {
markUserSkipped(invitedUser, finalRegionId, finalDeviceId);
return InviteRewardResult.skipped("当前位置不在邀请区域内");
}
InviteRewardConfig config = getEnabledConfig(finalRegionId);
if (config == null || config.getRewardAmount() == null || config.getRewardAmount().compareTo(BigDecimal.ZERO) <= 0) {
return InviteRewardResult.skipped("区域未配置拉新奖励");
}
if (hasAnyRewardRecord(invitedUser.getId())) {
return InviteRewardResult.skipped("该用户拉新奖励已处理");
}
if (hasSuccessfulReward(invitedUser.getId(), phoneNumber, finalDeviceId)) {
markUserSkipped(invitedUser, finalRegionId, finalDeviceId);
return InviteRewardResult.skipped("手机号或设备已领取过拉新奖励");
}
if (exceedDailyLimit(config) || exceedTotalBudget(config)) {
return InviteRewardResult.skipped("区域奖励预算或上限已用完");
}
InviteRewardRecord record = createProcessingRecord(invitedUser, phoneNumber, finalInviterId, finalRegionId, finalDeviceId, config.getRewardAmount());
WechatMerchantTransferResult transferResult = transferService.transferWithAuthorization(
inviter.getInviteTransferAuthId(),
inviter.getInviteTransferAuthNo(),
config.getRewardAmount(),
record.getOutBillNo()
);
Date now = new Date();
record.setUpdateTime(now);
record.setWxResponse(transferResult.getResponseBody());
record.setTransferBillNo(transferResult.getTransferBillNo());
if (transferResult.isSuccess()) {
record.setStatus(STATUS_SUCCESS);
recordMapper.updateById(record);
config.setUsedBudget((config.getUsedBudget() == null ? BigDecimal.ZERO : config.getUsedBudget()).add(config.getRewardAmount()));
config.setUpdateTime(now);
configMapper.updateById(config);
invitedUser.setInviteRegionId(finalRegionId);
invitedUser.setInviteDeviceId(finalDeviceId);
invitedUser.setInviteRewardStatus(STATUS_SUCCESS);
invitedUser.setInviteRewardAmount(config.getRewardAmount());
invitedUser.setInviteRewardTime(now);
userService.update(invitedUser);
return InviteRewardResult.rewarded(config.getRewardAmount(), "拉新奖励已发放");
}
if (transferResult.isProcessing()) {
record.setStatus(STATUS_PROCESSING);
record.setFailReason(StringUtils.defaultIfBlank(transferResult.getMessage(), "待邀请人确认收款"));
recordMapper.updateById(record);
invitedUser.setInviteRegionId(finalRegionId);
invitedUser.setInviteDeviceId(finalDeviceId);
invitedUser.setInviteRewardStatus(STATUS_PROCESSING);
userService.update(invitedUser);
return InviteRewardResult.processing(record.getFailReason());
}
record.setStatus(STATUS_FAILED);
record.setFailReason(StringUtils.defaultIfBlank(transferResult.getMessage(), "微信商家转账失败"));
recordMapper.updateById(record);
invitedUser.setInviteRegionId(finalRegionId);
invitedUser.setInviteDeviceId(finalDeviceId);
invitedUser.setInviteRewardStatus(STATUS_FAILED);
userService.update(invitedUser);
return InviteRewardResult.failed(record.getFailReason());
}
@Override
public InviteRewardConfig getEnabledConfig(String regionId) {
if (StringUtils.isBlank(regionId)) {
return null;
}
return configMapper.selectOne(new LambdaQueryWrapper<InviteRewardConfig>()
.eq(InviteRewardConfig::getRegionId, regionId)
.eq(InviteRewardConfig::getEnabled, 1)
.last("limit 1"));
}
private boolean isLocationInRegion(String regionId, Double lat, Double lng) {
if (StringUtils.isBlank(regionId) || lat == null || lng == null) {
return false;
}
ShopArea matched = shopAreaService.matchByLocation(lat, lng);
return matched != null && StringUtils.equals(regionId, matched.getId());
}
private boolean hasSuccessfulReward(String invitedUserId, String phoneNumber, String deviceId) {
Long userCount = recordMapper.selectCount(new LambdaQueryWrapper<InviteRewardRecord>()
.eq(InviteRewardRecord::getStatus, STATUS_SUCCESS)
.eq(InviteRewardRecord::getInvitedUserId, invitedUserId));
if (userCount != null && userCount > 0) {
return true;
}
if (StringUtils.isNotBlank(phoneNumber)) {
Long mobileCount = recordMapper.selectCount(new LambdaQueryWrapper<InviteRewardRecord>()
.eq(InviteRewardRecord::getStatus, STATUS_SUCCESS)
.eq(InviteRewardRecord::getInvitedMobile, phoneNumber));
if (mobileCount != null && mobileCount > 0) {
return true;
}
}
if (StringUtils.isNotBlank(deviceId)) {
Long deviceCount = recordMapper.selectCount(new LambdaQueryWrapper<InviteRewardRecord>()
.eq(InviteRewardRecord::getStatus, STATUS_SUCCESS)
.eq(InviteRewardRecord::getInviteDeviceId, deviceId));
return deviceCount != null && deviceCount > 0;
}
return false;
}
private boolean hasAnyRewardRecord(String invitedUserId) {
Long count = recordMapper.selectCount(new LambdaQueryWrapper<InviteRewardRecord>()
.eq(InviteRewardRecord::getInvitedUserId, invitedUserId));
return count != null && count > 0;
}
private boolean exceedDailyLimit(InviteRewardConfig config) {
if (config.getDailyLimit() == null || config.getDailyLimit() <= 0) {
return false;
}
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Long count = recordMapper.selectCount(new LambdaQueryWrapper<InviteRewardRecord>()
.eq(InviteRewardRecord::getRegionId, config.getRegionId())
.eq(InviteRewardRecord::getStatus, STATUS_SUCCESS)
.ge(InviteRewardRecord::getCreateTime, calendar.getTime()));
return count != null && count >= config.getDailyLimit();
}
private boolean exceedTotalBudget(InviteRewardConfig config) {
BigDecimal totalBudget = config.getTotalBudget() == null ? BigDecimal.ZERO : config.getTotalBudget();
if (totalBudget.compareTo(BigDecimal.ZERO) <= 0) {
return false;
}
BigDecimal usedBudget = config.getUsedBudget() == null ? BigDecimal.ZERO : config.getUsedBudget();
BigDecimal rewardAmount = config.getRewardAmount() == null ? BigDecimal.ZERO : config.getRewardAmount();
return usedBudget.add(rewardAmount).compareTo(totalBudget) > 0;
}
private InviteRewardRecord createProcessingRecord(User invitedUser, String phoneNumber, String inviterId, String regionId,
String deviceId, BigDecimal amount) {
Date now = new Date();
InviteRewardRecord record = new InviteRewardRecord();
record.setId(genId());
record.setInviterId(inviterId);
record.setInvitedUserId(invitedUser.getId());
record.setInvitedMobile(phoneNumber);
record.setInviteDeviceId(deviceId);
record.setRegionId(regionId);
record.setAmount(amount);
record.setStatus(STATUS_PROCESSING);
record.setOutBillNo(record.getId());
record.setCreateTime(now);
record.setUpdateTime(now);
recordMapper.insert(record);
return record;
}
private String buildAuthorizationNo(User user) {
String userId = user == null ? "" : StringUtils.defaultString(user.getId());
return ("IA" + userId).replaceAll("[^A-Za-z0-9]", "");
}
private void markUserSkipped(User user, String regionId, String deviceId) {
user.setInviteRegionId(regionId);
user.setInviteDeviceId(deviceId);
user.setInviteRewardStatus(STATUS_SKIPPED);
userService.update(user);
}
}

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

@ -4,6 +4,8 @@ import cc.hiver.core.common.exception.HiverException;
import cc.hiver.core.common.utils.ResultUtil; import cc.hiver.core.common.utils.ResultUtil;
import cc.hiver.core.common.vo.PageVo; import cc.hiver.core.common.vo.PageVo;
import cc.hiver.core.common.vo.Result; import cc.hiver.core.common.vo.Result;
import cc.hiver.mall.invite.entity.InviteRewardConfig;
import cc.hiver.mall.invite.service.InviteRewardService;
import cc.hiver.mall.planet.entity.*; import cc.hiver.mall.planet.entity.*;
import cc.hiver.mall.planet.service.PlanetAdminService; import cc.hiver.mall.planet.service.PlanetAdminService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@ -32,6 +34,9 @@ public class PlanetAdminController {
@Autowired @Autowired
private cc.hiver.mall.planet.service.PlanetAdventureService adventureService; private cc.hiver.mall.planet.service.PlanetAdventureService adventureService;
@Autowired
private InviteRewardService inviteRewardService;
/** 商圈强隔离:白嫖星球后台所有功能均限定在本商圈内 */ /** 商圈强隔离:白嫖星球后台所有功能均限定在本商圈内 */
private static final String REGION_REQUIRED = "请先选择商圈,白嫖星球仅限本区域内进行"; private static final String REGION_REQUIRED = "请先选择商圈,白嫖星球仅限本区域内进行";
@ -119,6 +124,45 @@ public class PlanetAdminController {
return ResultUtil.success("删除成功"); return ResultUtil.success("删除成功");
} }
// ---------------- 拉新返现配置 ----------------
@ApiOperation(value = "拉新返现配置分页")
@RequestMapping(value = "/inviteReward/page", method = RequestMethod.GET)
public Result inviteRewardPage(@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(inviteRewardService.pageConfigs(buildPage(pageNumber, pageSize), regionId));
}
@ApiOperation(value = "保存拉新返现配置")
@RequestMapping(value = "/inviteReward/save", method = RequestMethod.POST)
public Result inviteRewardSave(@RequestBody InviteRewardConfig config) {
if (config == null || StringUtils.isEmpty(config.getRegionId())) {
return ResultUtil.error("拉新返现配置必须归属商圈");
}
return new ResultUtil<>().setData(inviteRewardService.saveConfig(config));
}
@ApiOperation(value = "删除拉新返现配置")
@RequestMapping(value = "/inviteReward/delete", method = RequestMethod.POST)
public Result inviteRewardDelete(@RequestParam String id) {
inviteRewardService.deleteConfig(id);
return ResultUtil.success("删除成功");
}
@ApiOperation(value = "拉新返现记录分页")
@RequestMapping(value = "/inviteReward/record/page", method = RequestMethod.GET)
public Result inviteRewardRecordPage(@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(inviteRewardService.pageRecords(buildPage(pageNumber, pageSize), regionId, null));
}
// ---------------- BUFF ---------------- // ---------------- BUFF ----------------
@ApiOperation(value = "BUFF分页") @ApiOperation(value = "BUFF分页")
@RequestMapping(value = "/buff/page", method = RequestMethod.GET) @RequestMapping(value = "/buff/page", method = RequestMethod.GET)

5
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java

@ -68,7 +68,8 @@ public class WechatPaySigner {
} }
String path = httpUrl.encodedPath(); // 如 "/v3/pay/transactions/jsapi" String path = httpUrl.encodedPath(); // 如 "/v3/pay/transactions/jsapi"
String query = httpUrl.query(); // 如 "a=1&b=2",若无则为 null String query = httpUrl.encodedQuery(); // 如 "a=1&b=2",若无则为 null
String urlPath = path + (query == null ? "" : "?" + query);
String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32); String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
long timestamp = System.currentTimeMillis() / 1000; // 秒! long timestamp = System.currentTimeMillis() / 1000; // 秒!
@ -76,7 +77,7 @@ public class WechatPaySigner {
// 构建签名消息(严格按格式) // 构建签名消息(严格按格式)
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(method).append("\n") sb.append(method).append("\n")
.append(path).append("\n") .append(urlPath).append("\n")
.append(timestamp).append("\n") .append(timestamp).append("\n")
.append(nonceStr).append("\n") .append(nonceStr).append("\n")
.append(body == null ? "" : body).append("\n"); .append(body == null ? "" : body).append("\n");

48
hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql

@ -0,0 +1,48 @@
CREATE TABLE IF NOT EXISTS `t_invite_reward_config` (
`id` varchar(64) NOT NULL COMMENT '主键',
`region_id` varchar(64) NOT NULL COMMENT '区域id',
`reward_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '每个有效新用户奖励金额',
`daily_limit` int(11) NOT NULL DEFAULT '0' COMMENT '每日发放上限 0不限',
`total_budget` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总预算 0不限',
`used_budget` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已发放预算',
`enabled` int(11) NOT NULL DEFAULT '1' COMMENT '是否启用 1是 0否',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_invite_reward_config_region` (`region_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拉新返现配置';
CREATE TABLE IF NOT EXISTS `t_invite_reward_record` (
`id` varchar(64) NOT NULL COMMENT '主键',
`inviter_id` varchar(64) NOT NULL COMMENT '邀请人用户id',
`invited_user_id` varchar(64) NOT NULL COMMENT '被邀请人用户id',
`invited_mobile` varchar(32) DEFAULT NULL COMMENT '被邀请人手机号',
`invite_device_id` varchar(128) DEFAULT NULL COMMENT '邀请设备标识',
`region_id` varchar(64) NOT NULL COMMENT '发生奖励时的区域id',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '奖励金额',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态 0处理中 1成功 2失败 3跳过',
`out_bill_no` varchar(64) DEFAULT NULL COMMENT '微信商家转账商户单号',
`transfer_bill_no` varchar(128) DEFAULT NULL COMMENT '微信商家转账单号',
`fail_reason` varchar(512) DEFAULT NULL COMMENT '失败或跳过原因',
`wx_response` text COMMENT '微信响应',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_invite_reward_invited_user` (`invited_user_id`),
KEY `idx_invite_reward_mobile` (`invited_mobile`),
KEY `idx_invite_reward_device` (`invite_device_id`),
KEY `idx_invite_reward_region_status` (`region_id`, `status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拉新返现奖励记录';
ALTER TABLE `t_user`
ADD COLUMN `invite_region_id` varchar(64) DEFAULT NULL COMMENT '拉新来源区域id',
ADD COLUMN `invite_device_id` varchar(128) DEFAULT NULL COMMENT '拉新设备标识',
ADD COLUMN `invite_new_user` int(11) DEFAULT '0' COMMENT '是否邀请链路新用户 1是 0否',
ADD COLUMN `invite_reward_status` int(11) DEFAULT '0' COMMENT '拉新奖励状态 0未处理 1成功 2失败 3跳过',
ADD COLUMN `invite_reward_amount` decimal(10,2) DEFAULT '0.00' COMMENT '拉新奖励金额',
ADD COLUMN `invite_reward_time` datetime DEFAULT NULL COMMENT '拉新奖励时间',
ADD COLUMN `invite_transfer_auth_no` varchar(64) DEFAULT NULL COMMENT '拉新转账免确认授权商户单号',
ADD COLUMN `invite_transfer_auth_id` varchar(64) DEFAULT NULL COMMENT '拉新转账微信免确认授权单号',
ADD COLUMN `invite_transfer_auth_state` varchar(32) DEFAULT NULL COMMENT '拉新转账免确认授权状态',
ADD COLUMN `invite_transfer_auth_time` datetime DEFAULT NULL COMMENT '拉新转账免确认授权时间';

35
hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java

@ -11,6 +11,8 @@ import cc.hiver.core.common.vo.Result;
import cc.hiver.core.config.properties.HiverTokenProperties; import cc.hiver.core.config.properties.HiverTokenProperties;
import cc.hiver.core.entity.User; import cc.hiver.core.entity.User;
import cc.hiver.core.service.UserService; import cc.hiver.core.service.UserService;
import cc.hiver.mall.invite.service.InviteRewardResult;
import cc.hiver.mall.invite.service.InviteRewardService;
import cc.hiver.mall.service.mybatis.CustomerService; import cc.hiver.mall.service.mybatis.CustomerService;
import cc.hiver.social.entity.Social; import cc.hiver.social.entity.Social;
import cc.hiver.social.service.SocialService; import cc.hiver.social.service.SocialService;
@ -109,6 +111,9 @@ public class WechatController {
@Autowired @Autowired
private WechatPhoneService wechatPhoneService; private WechatPhoneService wechatPhoneService;
@Autowired
private InviteRewardService inviteRewardService;
@Autowired @Autowired
private RedisTemplateHelper redisTemplateHelper; private RedisTemplateHelper redisTemplateHelper;
@ -144,15 +149,37 @@ public class WechatController {
@PostMapping("/getPhone") @PostMapping("/getPhone")
@ResponseBody @ResponseBody
public Result getUserPhone(@RequestParam(required = false) String code) { public Result getUserPhone(@RequestParam(required = false) String code,
@RequestParam(required = false) String inviterId,
@RequestParam(required = false) String regionId,
@RequestParam(required = false) String deviceId,
@RequestParam(required = false) Double lat,
@RequestParam(required = false) Double lng) {
String phoneNumber = wechatPhoneService.getPhoneNumber(code); String phoneNumber = wechatPhoneService.getPhoneNumber(code);
if (CharSequenceUtil.isNotBlank(phoneNumber)) { if (CharSequenceUtil.isNotBlank(phoneNumber)) {
try { try {
final User u = securityUtil.getCurrUser(); final User u = securityUtil.getCurrUser();
String key = "username::" + u.getUsername(); String key = "username::" + u.getUsername();
boolean firstPhoneBind = CharSequenceUtil.isBlank(u.getMobile());
User phoneOwner = userService.findByMobile(phoneNumber);
if (phoneOwner != null && CharSequenceUtil.isNotBlank(phoneOwner.getId())
&& !phoneOwner.getId().equals(u.getId())) {
return ResultUtil.error("该手机号已绑定其他账号");
}
u.setMobile(phoneNumber); u.setMobile(phoneNumber);
if (CharSequenceUtil.isNotBlank(inviterId) && CharSequenceUtil.isBlank(u.getInviterId())) {
u.setInviterId(inviterId);
}
if (CharSequenceUtil.isNotBlank(regionId) && CharSequenceUtil.isBlank(u.getInviteRegionId())) {
u.setInviteRegionId(regionId);
}
if (CharSequenceUtil.isNotBlank(deviceId) && CharSequenceUtil.isBlank(u.getInviteDeviceId())) {
u.setInviteDeviceId(deviceId);
}
userService.update(u); userService.update(u);
InviteRewardResult rewardResult = inviteRewardService.rewardAfterPhoneBound(
u, phoneNumber, inviterId, regionId, deviceId, lat, lng, firstPhoneBind);
// 删除缓存 // 删除缓存
redisTemplate.delete(key); redisTemplate.delete(key);
//User newUser = userService.findByMobile(phoneNumber); //User newUser = userService.findByMobile(phoneNumber);
@ -161,9 +188,13 @@ public class WechatController {
final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd hh:mm:ss").create(); final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd hh:mm:ss").create();
redisTemplateHelper.set(keyNew, gson.toJson(u), tokenProperties.getSaveLoginTime(), TimeUnit.DAYS); redisTemplateHelper.set(keyNew, gson.toJson(u), tokenProperties.getSaveLoginTime(), TimeUnit.DAYS);
final String accessToken = securityUtil.getToken(userService.userToDTO(u), true); final String accessToken = securityUtil.getToken(userService.userToDTO(u), true);
Map<String,String> map = new HashMap<>(); Map<String,Object> map = new HashMap<>();
map.put("mobile",phoneNumber); map.put("mobile",phoneNumber);
map.put("accessToken",accessToken); map.put("accessToken",accessToken);
map.put("inviteRewarded", rewardResult != null && rewardResult.isRewarded());
map.put("inviteRewardProcessing", rewardResult != null && rewardResult.isProcessing());
map.put("inviteRewardAmount", rewardResult == null ? null : rewardResult.getAmount());
map.put("inviteRewardMessage", rewardResult == null ? "" : rewardResult.getMessage());
return ResultUtil.data(map); return ResultUtil.data(map);
} catch (Exception e) { } catch (Exception e) {
log.error("绑定微信手机号失败: phoneNumber={}", phoneNumber, e); log.error("绑定微信手机号失败: phoneNumber={}", phoneNumber, e);

Loading…
Cancel
Save