diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/controller/HanHaiMemberController.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/controller/HanHaiMemberController.java index ee5a0c6..bd99c78 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/controller/HanHaiMemberController.java +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/controller/HanHaiMemberController.java @@ -39,7 +39,7 @@ import org.jeecg.common.aspect.annotation.AutoLog; /** * @Description: han_hai_member * @Author: jeecg-boot - * @Date: 2025-05-25 + * @Date: 2025-06-13 * @Version: V1.0 */ @Api(tags="han_hai_member") diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/entity/HanHaiMember.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/entity/HanHaiMember.java index 9c0c237..dd70723 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/entity/HanHaiMember.java +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/entity/HanHaiMember.java @@ -20,7 +20,7 @@ import lombok.experimental.Accessors; /** * @Description: han_hai_member * @Author: jeecg-boot - * @Date: 2025-05-25 + * @Date: 2025-06-13 * @Version: V1.0 */ @Data @@ -215,4 +215,8 @@ public class HanHaiMember implements Serializable { @Excel(name = "被推广佣金", width = 15) @ApiModelProperty(value = "被推广佣金") private java.math.BigDecimal recommendedAmount; + /**累计提现*/ + @Excel(name = "累计提现", width = 15) + @ApiModelProperty(value = "累计提现") + private java.math.BigDecimal cashoutSum; } diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/mapper/HanHaiMemberMapper.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/mapper/HanHaiMemberMapper.java index fc89a3b..0ada299 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/mapper/HanHaiMemberMapper.java +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/mapper/HanHaiMemberMapper.java @@ -9,7 +9,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @Description: han_hai_member * @Author: jeecg-boot - * @Date: 2025-05-25 + * @Date: 2025-06-13 * @Version: V1.0 */ public interface HanHaiMemberMapper extends BaseMapper { diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/IHanHaiMemberService.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/IHanHaiMemberService.java index 0fdd5e0..3215186 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/IHanHaiMemberService.java +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/IHanHaiMemberService.java @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.extension.service.IService; /** * @Description: han_hai_member * @Author: jeecg-boot - * @Date: 2025-05-25 + * @Date: 2025-06-13 * @Version: V1.0 */ public interface IHanHaiMemberService extends IService { diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/impl/HanHaiMemberServiceImpl.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/impl/HanHaiMemberServiceImpl.java index cf564ce..d4d8b20 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/impl/HanHaiMemberServiceImpl.java +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/service/impl/HanHaiMemberServiceImpl.java @@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; /** * @Description: han_hai_member * @Author: jeecg-boot - * @Date: 2025-05-25 + * @Date: 2025-06-13 * @Version: V1.0 */ @Service diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/HanHaiMemberList.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/HanHaiMemberList.vue index 8b50c0b..6530808 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/HanHaiMemberList.vue +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/HanHaiMemberList.vue @@ -321,6 +321,11 @@ align:"center", dataIndex: 'recommendedAmount' }, + { + title:'累计提现', + align:"center", + dataIndex: 'cashoutSum' + }, { title: '操作', dataIndex: 'action', @@ -394,6 +399,7 @@ fieldList.push({type:'string',value:'isMerchants',text:'是否商家',dictCode:'is_merchants'}) fieldList.push({type:'BigDecimal',value:'recommendAmount',text:'推广佣金',dictCode:''}) fieldList.push({type:'BigDecimal',value:'recommendedAmount',text:'被推广佣金',dictCode:''}) + fieldList.push({type:'BigDecimal',value:'cashoutSum',text:'累计提现',dictCode:''}) this.superFieldList = fieldList } } diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/modules/HanHaiMemberForm.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/modules/HanHaiMemberForm.vue index b1b29d8..030f383 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/modules/HanHaiMemberForm.vue +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue/modules/HanHaiMemberForm.vue @@ -198,6 +198,11 @@ + + + + + diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue3/HanHaiMember.data.ts b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue3/HanHaiMember.data.ts index 15a5089..06e8fb3 100644 --- a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue3/HanHaiMember.data.ts +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/hanHaiMember/vue3/HanHaiMember.data.ts @@ -203,6 +203,11 @@ export const columns: BasicColumn[] = [ align:"center", dataIndex: 'recommendedAmount' }, + { + title: '累计提现', + align:"center", + dataIndex: 'cashoutSum' + }, ]; //查询数据 export const searchFormSchema: FormSchema[] = [ @@ -415,4 +420,9 @@ export const formSchema: FormSchema[] = [ field: 'recommendedAmount', component: 'InputNumber', }, + { + label: '累计提现', + field: 'cashoutSum', + component: 'InputNumber', + }, ]; diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/controller/MassageCashoutLogController.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/controller/MassageCashoutLogController.java new file mode 100644 index 0000000..a835565 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/controller/MassageCashoutLogController.java @@ -0,0 +1,171 @@ +package org.jeecg.modules.massageCashoutLog.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.massageCashoutLog.entity.MassageCashoutLog; +import org.jeecg.modules.massageCashoutLog.service.IMassageCashoutLogService; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.base.controller.JeecgController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import com.alibaba.fastjson.JSON; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.jeecg.common.aspect.annotation.AutoLog; + + /** + * @Description: 提现记录表 + * @Author: jeecg-boot + * @Date: 2025-06-13 + * @Version: V1.0 + */ +@Api(tags="提现记录表") +@RestController +@RequestMapping("/massageCashoutLog/massageCashoutLog") +@Slf4j +public class MassageCashoutLogController extends JeecgController { + @Autowired + private IMassageCashoutLogService massageCashoutLogService; + + /** + * 分页列表查询 + * + * @param massageCashoutLog + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "提现记录表-分页列表查询") + @ApiOperation(value="提现记录表-分页列表查询", notes="提现记录表-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(MassageCashoutLog massageCashoutLog, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(massageCashoutLog, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = massageCashoutLogService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param massageCashoutLog + * @return + */ + @AutoLog(value = "提现记录表-添加") + @ApiOperation(value="提现记录表-添加", notes="提现记录表-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody MassageCashoutLog massageCashoutLog) { + massageCashoutLogService.save(massageCashoutLog); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param massageCashoutLog + * @return + */ + @AutoLog(value = "提现记录表-编辑") + @ApiOperation(value="提现记录表-编辑", notes="提现记录表-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody MassageCashoutLog massageCashoutLog) { + massageCashoutLogService.updateById(massageCashoutLog); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "提现记录表-通过id删除") + @ApiOperation(value="提现记录表-通过id删除", notes="提现记录表-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + massageCashoutLogService.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "提现记录表-批量删除") + @ApiOperation(value="提现记录表-批量删除", notes="提现记录表-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.massageCashoutLogService.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "提现记录表-通过id查询") + @ApiOperation(value="提现记录表-通过id查询", notes="提现记录表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + MassageCashoutLog massageCashoutLog = massageCashoutLogService.getById(id); + if(massageCashoutLog==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(massageCashoutLog); + } + + /** + * 导出excel + * + * @param request + * @param massageCashoutLog + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, MassageCashoutLog massageCashoutLog) { + return super.exportXls(request, massageCashoutLog, MassageCashoutLog.class, "提现记录表"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, MassageCashoutLog.class); + } + +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/entity/MassageCashoutLog.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/entity/MassageCashoutLog.java new file mode 100644 index 0000000..51b53fc --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/entity/MassageCashoutLog.java @@ -0,0 +1,84 @@ +package org.jeecg.modules.massageCashoutLog.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.math.BigDecimal; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 提现记录表 + * @Author: jeecg-boot + * @Date: 2025-06-13 + * @Version: V1.0 + */ +@Data +@TableName("massage_cashout_log") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@ApiModel(value="massage_cashout_log对象", description="提现记录表") +public class MassageCashoutLog implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键*/ + @TableId(type = IdType.ASSIGN_ID) + @ApiModelProperty(value = "主键") + private java.lang.String id; + /**创建人*/ + @ApiModelProperty(value = "创建人") + private java.lang.String createBy; + /**创建日期*/ + @ApiModelProperty(value = "创建日期") + private java.util.Date createTime; + /**更新人*/ + @ApiModelProperty(value = "更新人") + private java.lang.String updateBy; + /**更新日期*/ + @ApiModelProperty(value = "更新日期") + private java.util.Date updateTime; + /**提现者姓名*/ + @Excel(name = "提现者姓名", width = 15) + @ApiModelProperty(value = "提现者姓名") + private java.lang.String realName; + /**提现金额*/ + @Excel(name = "提现金额", width = 15) + @ApiModelProperty(value = "提现金额") + private java.math.BigDecimal amount; + /**提现状态*/ + @Excel(name = "提现状态", width = 15, dicCode = "cashout_status") + @Dict(dicCode = "cashout_status") + @ApiModelProperty(value = "提现状态") + private java.lang.String status; + /**到账时间*/ + @Excel(name = "到账时间", width = 15) + @ApiModelProperty(value = "到账时间") + private java.util.Date paymentTime; + /**商户单号*/ + @Excel(name = "商户单号", width = 15) + @ApiModelProperty(value = "商户单号") + private java.lang.String outBillNo; + /**微信转账单号*/ + @Excel(name = "微信转账单号", width = 15) + @ApiModelProperty(value = "微信转账单号") + private java.lang.String transferBillNo; + /**领取转账信息参数*/ + @Excel(name = "领取转账信息参数", width = 15) + @ApiModelProperty(value = "领取转账信息参数") + private java.lang.String packageInfo; + /**关联用户id*/ + @Excel(name = "关联用户id", width = 15, dictTable = "han_hai_member", dicText = "nick_name", dicCode = "id") + @Dict(dictTable = "han_hai_member", dicText = "nick_name", dicCode = "id") + @ApiModelProperty(value = "关联用户id") + private java.lang.String userId; +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/MassageCashoutLogMapper.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/MassageCashoutLogMapper.java new file mode 100644 index 0000000..040d329 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/MassageCashoutLogMapper.java @@ -0,0 +1,17 @@ +package org.jeecg.modules.massageCashoutLog.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import org.jeecg.modules.massageCashoutLog.entity.MassageCashoutLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 提现记录表 + * @Author: jeecg-boot + * @Date: 2025-06-13 + * @Version: V1.0 + */ +public interface MassageCashoutLogMapper extends BaseMapper { + +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/xml/MassageCashoutLogMapper.xml b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/xml/MassageCashoutLogMapper.xml new file mode 100644 index 0000000..6b56a88 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/mapper/xml/MassageCashoutLogMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/IMassageCashoutLogService.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/IMassageCashoutLogService.java new file mode 100644 index 0000000..6adf867 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/IMassageCashoutLogService.java @@ -0,0 +1,14 @@ +package org.jeecg.modules.massageCashoutLog.service; + +import org.jeecg.modules.massageCashoutLog.entity.MassageCashoutLog; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 提现记录表 + * @Author: jeecg-boot + * @Date: 2025-06-13 + * @Version: V1.0 + */ +public interface IMassageCashoutLogService extends IService { + +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/impl/MassageCashoutLogServiceImpl.java b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/impl/MassageCashoutLogServiceImpl.java new file mode 100644 index 0000000..3e9e8e4 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/service/impl/MassageCashoutLogServiceImpl.java @@ -0,0 +1,19 @@ +package org.jeecg.modules.massageCashoutLog.service.impl; + +import org.jeecg.modules.massageCashoutLog.entity.MassageCashoutLog; +import org.jeecg.modules.massageCashoutLog.mapper.MassageCashoutLogMapper; +import org.jeecg.modules.massageCashoutLog.service.IMassageCashoutLogService; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 提现记录表 + * @Author: jeecg-boot + * @Date: 2025-06-13 + * @Version: V1.0 + */ +@Service +public class MassageCashoutLogServiceImpl extends ServiceImpl implements IMassageCashoutLogService { + +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/MassageCashoutLogList.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/MassageCashoutLogList.vue new file mode 100644 index 0000000..21fcd18 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/MassageCashoutLogList.vue @@ -0,0 +1,214 @@ + + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogForm.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogForm.vue new file mode 100644 index 0000000..23e3cd5 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogForm.vue @@ -0,0 +1,139 @@ + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.Style#Drawer.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.Style#Drawer.vue new file mode 100644 index 0000000..449fc57 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.Style#Drawer.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.vue new file mode 100644 index 0000000..a3c8328 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue/modules/MassageCashoutLogModal.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.api.ts b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.api.ts new file mode 100644 index 0000000..c7bc507 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.api.ts @@ -0,0 +1,61 @@ +import {defHttp} from '/@/utils/http/axios'; +import {Modal} from 'ant-design-vue'; + +enum Api { + list = '/massageCashoutLog/massageCashoutLog/list', + save='/massageCashoutLog/massageCashoutLog/add', + edit='/massageCashoutLog/massageCashoutLog/edit', + deleteOne = '/massageCashoutLog/massageCashoutLog/delete', + deleteBatch = '/massageCashoutLog/massageCashoutLog/deleteBatch', + importExcel = '/massageCashoutLog/massageCashoutLog/importExcel', + exportXls = '/massageCashoutLog/massageCashoutLog/exportXls', +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.data.ts b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.data.ts new file mode 100644 index 0000000..2edd30a --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLog.data.ts @@ -0,0 +1,99 @@ +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +//列表数据 +export const columns: BasicColumn[] = [ + { + title: '提现者姓名', + align:"center", + dataIndex: 'realName' + }, + { + title: '提现金额', + align:"center", + dataIndex: 'amount' + }, + { + title: '提现状态', + align:"center", + dataIndex: 'status_dictText' + }, + { + title: '到账时间', + align:"center", + dataIndex: 'paymentTime' + }, + { + title: '商户单号', + align:"center", + dataIndex: 'outBillNo' + }, + { + title: '微信转账单号', + align:"center", + dataIndex: 'transferBillNo' + }, + { + title: '领取转账信息参数', + align:"center", + dataIndex: 'packageInfo' + }, + { + title: '关联用户id', + align:"center", + dataIndex: 'userId_dictText' + }, +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +]; +//表单数据 +export const formSchema: FormSchema[] = [ + { + label: '提现者姓名', + field: 'realName', + component: 'Input', + }, + { + label: '提现金额', + field: 'amount', + component: 'InputNumber', + }, + { + label: '提现状态', + field: 'status', + component: 'JDictSelectTag', + componentProps:{ + dictCode:"cashout_status" + }, + }, + { + label: '到账时间', + field: 'paymentTime', + component: 'Input', + }, + { + label: '商户单号', + field: 'outBillNo', + component: 'Input', + }, + { + label: '微信转账单号', + field: 'transferBillNo', + component: 'Input', + }, + { + label: '领取转账信息参数', + field: 'packageInfo', + component: 'Input', + }, + { + label: '关联用户id', + field: 'userId', + component: 'JDictSelectTag', + componentProps:{ + dictCode:"han_hai_member,nick_name,id" + }, + }, +]; diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLogList.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLogList.vue new file mode 100644 index 0000000..a804a80 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/MassageCashoutLogList.vue @@ -0,0 +1,162 @@ + + + + + \ No newline at end of file diff --git a/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/components/MassageCashoutLogModal.vue b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/components/MassageCashoutLogModal.vue new file mode 100644 index 0000000..5f09f92 --- /dev/null +++ b/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/modules/massageCashoutLog/vue3/components/MassageCashoutLogModal.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/api/massageController/AmountController.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/api/massageController/AmountController.java new file mode 100644 index 0000000..6ae06f4 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/api/massageController/AmountController.java @@ -0,0 +1,56 @@ +package org.jeecg.modules.api.massageController; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.vo.Result; +import org.jeecg.modules.apiBean.PageBean; +import org.jeecg.modules.apiService.AmountService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.math.BigDecimal; + +@Api(tags="我的-钱包相关接口") +@RestController +@RequestMapping("/massage" + + "" + + "/amount") +@Slf4j +public class AmountController { + + /******************************************************************************************************************/ + @Resource + private AmountService amountService; + /******************************************************************************************************************/ + @ApiOperation(value="钱包-提现记录列表", notes="钱包-提现记录列表") + @RequestMapping(value = "/queryCashoutLog", method = {RequestMethod.GET}) + public Result queryCashoutLog(@RequestHeader("X-Access-Token") String token, PageBean pageBean){ + return amountService.queryCashoutLog(token, pageBean); + } + + @ApiOperation(value="钱包-提现", notes="钱包-提现") + @RequestMapping(value = "/cashout", method = {RequestMethod.POST}) + public Result cashout(@RequestHeader("X-Access-Token") String token, String userName, BigDecimal transferAmount){ + return amountService.cashOut(token, userName, transferAmount); + } + + @ApiOperation(value="钱包-领取提现金额", notes="钱包-领取提现金额") + @RequestMapping(value = "/getMoney", method = {RequestMethod.POST}) + public Result getMoney(@RequestHeader("X-Access-Token") String token, String id){ + return amountService.getMoney(token, id); + } + + //提现回调 + @PostMapping("/cashoutNotify") + public Object cashoutNotify(@RequestHeader("Wechatpay-Signature") String signature, + @RequestHeader("Wechatpay-Timestamp") String timestamp, + @RequestHeader("Wechatpay-Nonce") String nonce, + @RequestHeader("Wechatpay-Serial") String serial, + @RequestBody String requestBody){ + return amountService.cashoutNotify(signature, timestamp, nonce, serial, requestBody); + } + + + +} diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/AmountService.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/AmountService.java new file mode 100644 index 0000000..44c1253 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/AmountService.java @@ -0,0 +1,29 @@ +package org.jeecg.modules.apiService; + +import org.jeecg.common.api.vo.Result; +import org.jeecg.modules.apiBean.PageBean; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.math.BigDecimal; + +public interface AmountService { + + //提现记录列表 + public Result queryCashoutLog(String token, PageBean pageBean); + //提现 + public Result cashOut(String token, String userName, BigDecimal transferAmount); + + //领取提现金额 + public Result getMoney(String token, String id); + + //提现回调 + @PostMapping("/cashoutNotify") + public Object cashoutNotify(@RequestHeader("Wechatpay-Signature") String signature, + @RequestHeader("Wechatpay-Timestamp") String timestamp, + @RequestHeader("Wechatpay-Nonce") String nonce, + @RequestHeader("Wechatpay-Serial") String serial, + @RequestBody String requestBody); + +} diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/impl/AmountServiceImpl.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/impl/AmountServiceImpl.java new file mode 100644 index 0000000..6bb204a --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/apiService/impl/AmountServiceImpl.java @@ -0,0 +1,342 @@ +package org.jeecg.modules.apiService.impl; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.map.HashedMap; +import org.apache.commons.lang.StringUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.config.shiro.ShiroRealm; +import org.jeecg.modules.apiBean.PageBean; +import org.jeecg.modules.apiService.AmountService; +import org.jeecg.modules.apiUtils.CommonUtils; +import org.jeecg.modules.hanHaiMember.entity.HanHaiMember; +import org.jeecg.modules.hanHaiMember.service.IHanHaiMemberService; +import org.jeecg.modules.massageCashoutLog.entity.MassageCashoutLog; +import org.jeecg.modules.massageCashoutLog.service.IMassageCashoutLogService; +import org.jeecg.modules.transfer.TransferToUser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Map; + +@Slf4j +@Service +public class AmountServiceImpl implements AmountService { + /*************************************************************************************/ + //微信小程序的 AppID + @Value("${wx.miniapp.appid}") + private String appid; + //微信小程序的密钥 + @Value("${wx.miniapp.secret}") + private String secret; + //商户号 + @Value("${wx.miniapp.merchantId}") + private String mchid; + //商户APIV3密钥 + @Value("${wx.miniapp.apiV3Key}") + private String apiV3Key; + //商户API私钥路径 + @Value("${wx.miniapp.privateKeyPath}") + private String privateKeyFilePath; + //商户API公钥路径 + @Value("${wx.miniapp.publicKeyPath}") + private String wechatPayPublicKeyFilePath; + //商户API公钥ID + @Value("${wx.miniapp.publicKeyId}") + private String wechatPayPublicKeyId; + //商户证书序列号 + @Value("${wx.miniapp.merchantSerialNumber}") + private String certiticateSerialNo; + + + //权限验证 + @Resource + private ShiroRealm shiroRealm; + //用户信息 + @Resource + private IHanHaiMemberService hanHaiMemberService; + //用户信息 + @Resource + private IMassageCashoutLogService massageCashoutLogService; + /*************************************************************************************/ + + @Override + public Result queryCashoutLog(String token, PageBean pageBean) { + log.info("获取提现记录列表开始"); + //权限验证 + HanHaiMember hanHaiMember = shiroRealm.checkUserTokenIsEffectHanHaiOpenId(token); + //HanHaiMember hanHaiMember = hanHaiMemberService.getById("1919297392365035521"); + //返回信息 + String massege = ""; + //分页信息 + Page page = null; + //查询信息 + LambdaQueryChainWrapper query = null; + //返回信息 + Page pageList = null; + + try{ + //分页 + page = new Page(pageBean.getPageNo(), pageBean.getPageSize()); + query = massageCashoutLogService + .lambdaQuery(); + + //组装查询条件 + query.eq(MassageCashoutLog::getUserId, hanHaiMember.getId()); + + //按照创建时间降序排列 + query.orderByDesc(MassageCashoutLog::getCreateTime); + + //获取轮播图信息 + pageList = query.page(page); + + log.info("获取提现记录列表结束"); + return Result.OK("提现记录列表", pageList); + }catch (Exception e){ + e.printStackTrace(); + log.info("获取提现记录列表失败{}", e.getMessage()); + return Result.error("提现记录列表查询失败"); + } + } + + //提现 + @Override + public Result cashOut(String token, String userName, BigDecimal transferAmount) { + //权限验证 + HanHaiMember hanHaiMember = shiroRealm.checkUserTokenIsEffectHanHaiOpenId(token); + //HanHaiMember hanHaiMember = hanHaiMemberService.getById("1919297392365035521"); + + //提现结果 + String massage = "提现申请失败"; + + try{ + //0、基础约束 + BigDecimal balance = hanHaiMember.getRecommendAmount();//用户推广佣金 + //金额不能为空 + if(null == transferAmount){ + log.info("金额为空,请填写大于0的整数金额"); + return Result.error("金额小于0,请填写大于0的整数金额"); + } + if(transferAmount.compareTo(balance)>0){ + //提现金额大于推广佣金 + log.info("推广佣金不足,当前推广佣金:{}", balance); + return Result.error("推广佣金不足"); + } + //提现金额要为整数 + if(transferAmount.scale()>0){ + log.info("请填写大于0的整数金额,当前输入金额:{}", transferAmount); + return Result.error("请填写大于0的整数金额"); + } + //提现金额大于0的整数 + if(transferAmount.compareTo(new BigDecimal(0))<=0){ + log.info("请填写大于0的整数金额,当前输入金额:{}", transferAmount); + return Result.error("请填写大于0的整数金额"); + } + if(StringUtils.isEmpty(userName)){ + log.info("用户姓名未填写"); + return Result.error("用户姓名未填写"); + } + + + //1.微信提现基础参数 + Map map = getMap(); + //变化的用户信息参数 + map.put("openid", hanHaiMember.getAppletOpenid());//用户openid + map.put("userName", userName);//用户真实姓名 + map.put("transferAmount", transferAmount);//提现金额, 单位为“分” + String idStr = "H" + IdWorker.getIdStr(); + map.put("outBillNo", idStr);//商户单号 + + TransferToUser client = new TransferToUser( + map.get("mchid").toString(), // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + map.get("certiticateSerialNo").toString(), // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + map.get("privateKeyFilePath").toString(), // 商户API证书私钥文件路径,本地文件路径 + map.get("wechatPayPublicKeyId").toString(), // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + map.get("wechatPayPublicKeyFilePath").toString() // 微信支付公钥文件路径,本地文件路径 + ); + + //2、场景信息 + TransferToUser.TransferToUserRequest request = new TransferToUser.TransferToUserRequest(); + request.appid = map.get("appid").toString(); + request.outBillNo = map.get("outBillNo").toString(); + request.transferSceneId = map.get("transferSceneId").toString(); + request.openid = map.get("openid").toString(); + request.userName = client.encrypt(map.get("userName").toString()); + request.transferAmount = transferAmount.longValue()*100;//单位为分 + request.transferRemark = map.get("transferRemark").toString(); + request.notifyUrl = map.get("notifyUrl").toString(); + request.userRecvPerception = map.get("userRecvPerception").toString(); + request.transferSceneReportInfos = new ArrayList<>(); + { + TransferToUser.TransferSceneReportInfo item0 = new TransferToUser.TransferSceneReportInfo(); + item0.infoType = map.get("infoType1").toString(); + item0.infoContent = map.get("infoContent1").toString(); + request.transferSceneReportInfos.add(item0); + TransferToUser.TransferSceneReportInfo item1 = new TransferToUser.TransferSceneReportInfo(); + item1.infoType = map.get("infoType2").toString(); + item1.infoContent = map.get("infoContent2").toString(); + request.transferSceneReportInfos.add(item1); + } + + //3、执行提现 + TransferToUser.TransferToUserResponse response = client.run(request, map); + log.info("提现发起成功,outBillNo:"+response.outBillNo + ",transferBillNo:" +response.transferBillNo + ",state:" +response.state+ ",packageInfo:" + response.packageInfo); + switch (response.state){ + case ACCEPTED: + log.info("转账已受理"); + massage = "转账已受理"; + break; + case PROCESSING: + log.info("转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试"); + massage = "转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试"; + break; + case WAIT_USER_CONFIRM: + log.info("待收款用户确认,可拉起微信收款确认页面进行收款确认"); + massage = "待收款用户确认,可拉起微信收款确认页面进行收款确认"; + break; + case TRANSFERING: + log.info("转账中,可拉起微信收款确认页面再次重试确认收款"); + massage = "转账中,可拉起微信收款确认页面再次重试确认收款"; + break; + case SUCCESS: + log.info("转账成功"); + massage = "转账成功"; + break; + case FAIL: + log.info("转账失败"); + massage = "转账失败"; + break; + case CANCELING: + log.info("商户撤销请求受理成功,该笔转账正在撤销中"); + massage = "商户撤销请求受理成功,该笔转账正在撤销中"; + break; + case CANCELLED: + log.info("转账撤销完成"); + massage = "转账撤销完成"; + break; + } + + log.info("提现结果:" + massage); + + //4、业务处理 + MassageCashoutLog cashoutLog = new MassageCashoutLog(); + cashoutLog.setAmount(transferAmount); + cashoutLog.setRealName(userName); + cashoutLog.setId(response.outBillNo); + cashoutLog.setOutBillNo(response.outBillNo); + cashoutLog.setTransferBillNo(response.transferBillNo); + cashoutLog.setPackageInfo(response.packageInfo); + cashoutLog.setUserId(hanHaiMember.getId()); + massageCashoutLogService.save(cashoutLog); + //推广佣金减少 + BigDecimal oldBalance = hanHaiMember.getRecommendAmount(); + BigDecimal newBalance = oldBalance.subtract(cashoutLog.getAmount()); + hanHaiMember.setRecommendAmount(newBalance); + hanHaiMemberService.updateById(hanHaiMember); + + //5、返回信息 + return Result.OK(massage, response); + }catch (Exception e){ + e.printStackTrace(); + log.info("提现失败:" + e.getMessage()); + return Result.error(e.getMessage()); + + } + } + + //领取提现金额 + @Override + public Result getMoney(String token, String id) { + log.info("领取提现金额"); + HanHaiMember hanHaiMember = shiroRealm.checkUserTokenIsEffectHanHaiOpenId(token); + + try{ + MassageCashoutLog cashoutLog = massageCashoutLogService + .lambdaQuery() + .eq(MassageCashoutLog::getUserId, hanHaiMember.getId()) + .eq(MassageCashoutLog::getId, id) + .eq(MassageCashoutLog::getStatus, "0") + .one(); + if(null != cashoutLog){ + //提现金额增加 + BigDecimal oldCashoutSum = hanHaiMember.getCashoutSum(); + BigDecimal newCashoutSum = oldCashoutSum.add(cashoutLog.getAmount()); + hanHaiMember.setCashoutSum(newCashoutSum); + hanHaiMemberService.updateById(hanHaiMember); + + //提现日志记录 + cashoutLog.setStatus("1"); + cashoutLog.setPaymentTime(CommonUtils.getCurrentTime()); + massageCashoutLogService.updateById(cashoutLog); + + log.info("领取提现金额结束"); + return Result.OK("领取提现结束"); + }else { + log.error("领取提现金额失败"); + return Result.error("提现内容不存在,请检查提现记录id:" + id); + } + + }catch (Exception e){ + e.printStackTrace(); + log.error("领取提现金额失败"); + return Result.OK("领取提现失败"); + } + } + + @Override + public Object cashoutNotify(String signature, String timestamp, String nonce, String serial, String requestBody) { + return null; + } + + /** + * 微信提现基础参数 + * @return + */ + public Map getMap(){ + Map map = new HashedMap();//转账接口所需参数 + map.put("host", "https://api.mch.weixin.qq.com");//请求地址 + map.put("method", "POST");//请求类型 + map.put("path", "/v3/fund-app/mch-transfer/transfer-bills");//提现接口 + map.put("notifyUrl", "https://www.yurangongfang.com/massage-admin/massage/cash/cashoutNotify/");//回调接口 + + //微信商户参数 +// map.put("appid", "wx77ba4c7131677a74");//小程序appid +// map.put("mchid", "1712378227");//商户号 +// map.put("certiticateSerialNo", "33E9FE8076531A7C7AD401DC34E053DBD7C28E22");//商户序列号 +// map.put("privateKeyFilePath", "jeecg-boot-module-system/src/main/resources/apiclient_key.pem");//商户私钥证书 +// map.put("wechatPayPublicKeyId", "PUB_KEY_ID_0117123782272025033100396400002931");//商户公钥id +// map.put("wechatPayPublicKeyFilePath", "jeecg-boot-module-system/src/main/resources/pub_key.pem");//商户公钥证书 + map.put("appid", appid);//小程序appid + map.put("mchid", mchid);//商户号 + map.put("certiticateSerialNo", certiticateSerialNo);//商户序列号 + map.put("privateKeyFilePath", privateKeyFilePath);//商户私钥证书 + map.put("wechatPayPublicKeyId", wechatPayPublicKeyId);//商户公钥id + map.put("wechatPayPublicKeyFilePath", wechatPayPublicKeyFilePath);//商户公钥证书 + map.put("transferSceneId", "1005");//商户转账场景ID 1005-佣金报酬 + map.put("transferRemark", "佣金报酬");//商户转账场景ID 1005-佣金报酬 + map.put("userRecvPerception", "劳务报酬");//商户转账场景ID 1005-佣金报酬 + + + //变化的用户信息参数 +// map.put("openid", "oFzrW4migndUepy7zYgYO2YoZ5to");//用户openid +// map.put("userName", "唐斌");//用户真实姓名 +// map.put("transferAmount", 100L);//提现金额, 单位为“分” +// String idStr = "H" + IdWorker.getIdStr(); +// map.put("outBillNo", idStr);//商户单号 + + + //转账场景报备信息,ransfer_scene_report_infos为数组类型参数,在现金营销的转账场景下需固定传两条明细,每条“转账场景报备信息明细”包含info_type、info_content两个参数。 + map.put("infoType1","岗位类型"); + map.put("infoContent1","外卖员"); + map.put("infoType2","报酬说明"); + map.put("infoContent2","高温补贴"); + + return map; + + } +} diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/TransferToUser.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/TransferToUser.java new file mode 100644 index 0000000..b2e0db6 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/TransferToUser.java @@ -0,0 +1,318 @@ +package org.jeecg.modules.transfer; + +import com.google.gson.annotations.SerializedName; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.List; +import java.util.Map; + +/** + * 发起转账 + */ +@Slf4j +public class TransferToUser { + + private final String mchid; + private final String certificateSerialNo; + private final PrivateKey privateKey; + private final String wechatPayPublicKeyId; + private final PublicKey wechatPayPublicKey; + + public TransferToUser(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { + this.mchid = mchid; + this.certificateSerialNo = certificateSerialNo; + this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath); + this.wechatPayPublicKeyId = wechatPayPublicKeyId; + this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); + } + +// /** +// * 微信提现基础参数 +// * @return +// */ +// public static Map getMap(){ +// Map map = new HashedMap();//转账接口所需参数 +// map.put("host", "https://api.mch.weixin.qq.com");//请求地址 +// map.put("method", "POST");//请求类型 +// map.put("path", "/v3/fund-app/mch-transfer/transfer-bills");//提现接口 +// map.put("notifyUrl", "https://www.yurangongfang.com/massage-admin/massage/cash/cashoutNotify/");//回调接口 +// +//// //微信商户参数 +//// map.put("appid", "wx77ba4c7131677a74");//小程序appid +//// map.put("mchid", "1712378227");//商户号 +//// map.put("certiticateSerialNo", "33E9FE8076531A7C7AD401DC34E053DBD7C28E22");//商户序列号 +//// map.put("privateKeyFilePath", "jeecg-boot-module-system/src/main/resources/apiclient_key.pem");//商户私钥证书 +//// map.put("wechatPayPublicKeyId", "PUB_KEY_ID_0117123782272025033100396400002931");//商户公钥id +//// map.put("wechatPayPublicKeyFilePath", "jeecg-boot-module-system/src/main/resources/pub_key.pem");//商户公钥证书 +//// map.put("transferSceneId", "1005");//商户转账场景ID 1005-佣金报酬 +//// map.put("transferRemark", "佣金报酬");//商户转账场景ID 1005-佣金报酬 +//// map.put("userRecvPerception", "劳务报酬");//商户转账场景ID 1005-佣金报酬 +// +// //微信商户参数(瑶都万能墙测试参数) +// map.put("appid", "wxa4d29e67e8a58d38");//小程序appid +// map.put("mchid", "1673516176");//商户号 +// map.put("certiticateSerialNo", "246ED77A7F882A59FD79993D09FDD2BA9A868FFE");//商户序列号 +// map.put("privateKeyFilePath", "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem");//商户私钥证书 +// map.put("wechatPayPublicKeyId", "PUB_KEY_ID_0116735161762025040100448900000949");//商户公钥id +// map.put("wechatPayPublicKeyFilePath", "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem");//商户公钥证书 +// map.put("transferSceneId", "1005");//商户转账场景ID 1005-佣金报酬 +// map.put("transferRemark", "佣金报酬");//商户转账场景ID 1005-佣金报酬 +// map.put("userRecvPerception", "劳务报酬");//商户转账场景ID 1005-佣金报酬 +// +// +// //变化的用户信息参数 +//// map.put("openid", "oFzrW4migndUepy7zYgYO2YoZ5to");//用户openid +//// map.put("userName", "用户真实姓名");//用户真实姓名 +// map.put("transferAmount", 100L);//提现金额, 单位为“分” +// String idStr = "H" + IdWorker.getIdStr(); +// map.put("outBillNo", idStr);//商户单号 +// +// +// //转账场景报备信息,ransfer_scene_report_infos为数组类型参数,在现金营销的转账场景下需固定传两条明细,每条“转账场景报备信息明细”包含info_type、info_content两个参数。 +// map.put("infoType1","岗位类型"); +// map.put("infoContent1","外卖员"); +// map.put("infoType2","报酬说明"); +// map.put("infoContent2","高温补贴"); +// +// return map; +// +// } +// +// public static void main(String[] args) { +// // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756 +// +// Map map = getMap(); +// TransferToUser client = new TransferToUser( +// map.get("mchid").toString(), // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 +// map.get("certiticateSerialNo").toString(), // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 +// map.get("privateKeyFilePath").toString(), // 商户API证书私钥文件路径,本地文件路径 +// map.get("wechatPayPublicKeyId").toString(), // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 +// map.get("wechatPayPublicKeyFilePath").toString() // 微信支付公钥文件路径,本地文件路径 +// ); +// +// TransferToUserRequest request = new TransferToUserRequest(); +// request.appid = map.get("appid").toString(); +// String idStr = "H" + IdWorker.getIdStr(); +// request.outBillNo = idStr; +// request.transferSceneId = map.get("transferSceneId").toString(); +// request.openid = map.get("openid").toString(); +// request.userName = client.encrypt(map.get("userName").toString()); +// request.transferAmount = 100L; +// request.transferRemark = map.get("transferRemark").toString(); +// request.notifyUrl = map.get("notifyUrl").toString(); +// request.userRecvPerception = map.get("userRecvPerception").toString(); +// request.transferSceneReportInfos = new ArrayList<>(); +// { +// TransferSceneReportInfo item0 = new TransferSceneReportInfo(); +// item0.infoType = map.get("infoType1").toString(); +// item0.infoContent = map.get("infoContent1").toString(); +// request.transferSceneReportInfos.add(item0); +// TransferSceneReportInfo item1 = new TransferSceneReportInfo(); +// item1.infoType = map.get("infoType2").toString(); +// item1.infoContent = map.get("infoContent2").toString(); +// request.transferSceneReportInfos.add(item1); +// }; +// try { +// TransferToUserResponse response = client.run(request, map); +// // TODO: 请求成功,继续业务逻辑 +// System.out.println(response); +// } catch (WXPayUtility.ApiException e) { +// // TODO: 请求失败,根据状态码执行不同的逻辑 +// e.printStackTrace(); +// } +// } +// +// +// public TransferToUserResponse run(Map map){ +// TransferToUser client = new TransferToUser( +// map.get("mchid").toString(), // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 +// map.get("certiticateSerialNo").toString(), // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 +// map.get("privateKeyFilePath").toString(), // 商户API证书私钥文件路径,本地文件路径 +// map.get("wechatPayPublicKeyId").toString(), // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 +// map.get("wechatPayPublicKeyFilePath").toString() // 微信支付公钥文件路径,本地文件路径 +// ); +// +// TransferToUserRequest request = new TransferToUserRequest(); +// +// +// request.appid = map.get("appid").toString(); +// request.outBillNo = map.get("outBillNo").toString(); +// request.transferSceneId = map.get("transferSceneId").toString(); +// request.openid = map.get("openid").toString(); +// request.userName = client.encrypt(map.get("userName").toString()); +// request.transferAmount = 100L; +// request.transferRemark = map.get("transferRemark").toString(); +// request.notifyUrl = map.get("notifyUrl").toString(); +// request.userRecvPerception = map.get("userRecvPerception").toString(); +// request.transferSceneReportInfos = new ArrayList<>(); +// { +// TransferSceneReportInfo item0 = new TransferSceneReportInfo(); +// item0.infoType = map.get("infoType1").toString(); +// item0.infoContent = map.get("infoContent1").toString(); +// request.transferSceneReportInfos.add(item0); +// TransferSceneReportInfo item1 = new TransferSceneReportInfo(); +// item1.infoType = map.get("infoType2").toString(); +// item1.infoContent = map.get("infoContent2").toString(); +// request.transferSceneReportInfos.add(item1); +// }; +// try { +// TransferToUserResponse response = client.run(request, map); +// // TODO: 请求成功,继续业务逻辑 +// log.info("提现发起成功,outBillNo:"+response.outBillNo + ",transferBillNo:" +response.transferBillNo + ",state:" +response.state); +// //转账结果 +// switch (response.state){ +// case ACCEPTED: +// log.info("转账已受理"); +// break; +// case PROCESSING: +// log.info("转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试"); +// break; +// case WAIT_USER_CONFIRM: +// log.info("待收款用户确认,可拉起微信收款确认页面进行收款确认"); +// break; +// case TRANSFERING: +// log.info("转账中,可拉起微信收款确认页面再次重试确认收款"); +// break; +// case SUCCESS: +// log.info("转账成功"); +// break; +// case FAIL: +// log.info("转账失败"); +// break; +// case CANCELING: +// log.info("商户撤销请求受理成功,该笔转账正在撤销中"); +// break; +// case CANCELLED: +// log.info("转账撤销完成"); +// break; +// } +// log.info("提现发起完成"); +// return response; +// } catch (WXPayUtility.ApiException e) { +// // TODO: 请求失败,根据状态码执行不同的逻辑 +// log.info("提现发起失败"); +// e.printStackTrace(); +// return null; +// } +// } + + public TransferToUserResponse run(TransferToUserRequest request, Map map) { + String uri = map.get("path").toString(); + String host = map.get("host").toString(); + String method = map.get("method").toString(); + String reqBody = WXPayUtility.toJson(request); + + Request.Builder reqBuilder = new Request.Builder().url(host + uri); + reqBuilder.addHeader("Accept", "application/json"); + reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); + reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, method, uri, reqBody)); + reqBuilder.addHeader("Content-Type", "application/json"); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); + reqBuilder.method(method, requestBody); + Request httpRequest = reqBuilder.build(); + + // 发送HTTP请求 + OkHttpClient client = new OkHttpClient.Builder().build(); + try (Response httpResponse = client.newCall(httpRequest).execute()) { + String respBody = WXPayUtility.extractBody(httpResponse); + if (httpResponse.code() >= 200 && httpResponse.code() < 300) { + // 2XX 成功,验证应答签名 + WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, + httpResponse.headers(), respBody); + + // 从HTTP应答报文构建返回数据 + return WXPayUtility.fromJson(respBody, TransferToUserResponse.class); + } else { + throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); + } + } catch (IOException e) { + throw new UncheckedIOException("Sending request to " + uri + " failed.", e); + } + } + + public String encrypt(String plainText) { + return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText); + } + + public static class TransferToUserResponse { + @SerializedName("out_bill_no") + public String outBillNo; + + @SerializedName("transfer_bill_no") + public String transferBillNo; + + @SerializedName("create_time") + public String createTime; + + @SerializedName("state") + public TransferBillStatus state; + + @SerializedName("package_info") + public String packageInfo; + } + + public enum TransferBillStatus { + @SerializedName("ACCEPTED") + ACCEPTED, + @SerializedName("PROCESSING") + PROCESSING, + @SerializedName("WAIT_USER_CONFIRM") + WAIT_USER_CONFIRM, + @SerializedName("TRANSFERING") + TRANSFERING, + @SerializedName("SUCCESS") + SUCCESS, + @SerializedName("FAIL") + FAIL, + @SerializedName("CANCELING") + CANCELING, + @SerializedName("CANCELLED") + CANCELLED + } + + public static class TransferSceneReportInfo { + @SerializedName("info_type") + public String infoType; + + @SerializedName("info_content") + public String infoContent; + } + + public static class TransferToUserRequest { + @SerializedName("appid") + public String appid; + + @SerializedName("out_bill_no") + public String outBillNo; + + @SerializedName("transfer_scene_id") + public String transferSceneId; + + @SerializedName("openid") + public String openid; + + @SerializedName("user_name") + public String userName; + + @SerializedName("transfer_amount") + public Long transferAmount; + + @SerializedName("transfer_remark") + public String transferRemark; + + @SerializedName("notify_url") + public String notifyUrl; + + @SerializedName("user_recv_perception") + public String userRecvPerception; + + @SerializedName("transfer_scene_report_infos") + public List transferSceneReportInfos; + } + +} diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/WXPayUtility.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/WXPayUtility.java new file mode 100644 index 0000000..030362e --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/transfer/WXPayUtility.java @@ -0,0 +1,381 @@ +package org.jeecg.modules.transfer; + +import com.google.gson.*; +import com.google.gson.annotations.Expose; +import okhttp3.Headers; +import okhttp3.Response; +import okio.BufferedSource; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Map; +import java.util.Objects; + +public class WXPayUtility { + private static final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.serialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .addDeserializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.deserialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .create(); + private static final char[] SYMBOLS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static final SecureRandom random = new SecureRandom(); + + /** + * 将 Object 转换为 JSON 字符串 + */ + public static String toJson(Object object) { + return gson.toJson(object); + } + + /** + * 将 JSON 字符串解析为特定类型的实例 + */ + public static T fromJson(String json, Class classOfT) throws JsonSyntaxException { + return gson.fromJson(json, classOfT); + } + + /** + * 从公私钥文件路径中读取文件内容 + * + * @param keyPath 文件路径 + * @return 文件内容 + */ + private static String readKeyStringFromPath(String keyPath) { + try { + return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 + * + * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePrivate( + new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的私钥文件中加载私钥 + * + * @param keyPath 私钥文件路径 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromPath(String keyPath) { + return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 + * + * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePublic( + new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的公钥文件中加载公钥 + * + * @param keyPath 公钥文件路径 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromPath(String keyPath) { + return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 + */ + public static String createNonce(int length) { + char[] buf = new char[length]; + for (int i = 0; i < length; ++i) { + buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; + } + return new String(buf); + } + + /** + * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 + * + * @param publicKey 加密用公钥对象 + * @param plaintext 待加密明文 + * @return 加密后密文 + */ + public static String encrypt(PublicKey publicKey, String plaintext) { + final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalArgumentException("Plaintext is too long", e); + } + } + + /** + * 使用私钥按照指定算法进行签名 + * + * @param message 待签名串 + * @param algorithm 签名算法,如 SHA256withRSA + * @param privateKey 签名用私钥对象 + * @return 签名结果 + */ + public static String sign(String message, String algorithm, PrivateKey privateKey) { + byte[] sign; + try { + Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(message.getBytes(StandardCharsets.UTF_8)); + sign = signature.sign(); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); + } catch (SignatureException e) { + throw new RuntimeException("An error occurred during the sign process.", e); + } + return Base64.getEncoder().encodeToString(sign); + } + + /** + * 使用公钥按照特定算法验证签名 + * + * @param message 待签名串 + * @param signature 待验证的签名内容 + * @param algorithm 签名算法,如:SHA256withRSA + * @param publicKey 验签用公钥对象 + * @return 签名验证是否通过 + */ + public static boolean verify(String message, String signature, String algorithm, + PublicKey publicKey) { + try { + Signature sign = Signature.getInstance(algorithm); + sign.initVerify(publicKey); + sign.update(message.getBytes(StandardCharsets.UTF_8)); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (SignatureException e) { + return false; + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("verify uses an illegal publickey.", e); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); + } + } + + /** + * 根据微信支付APIv3请求签名规则构造 Authorization 签名 + * + * @param mchid 商户号 + * @param certificateSerialNo 商户API证书序列号 + * @param privateKey 商户API证书私钥 + * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE + * @param uri 请求接口的URL + * @param body 请求接口的Body + * @return 构造好的微信支付APIv3 Authorization 头 + */ + public static String buildAuthorization(String mchid, String certificateSerialNo, + PrivateKey privateKey, + String method, String uri, String body) { + String nonce = createNonce(32); + long timestamp = Instant.now().getEpochSecond(); + + String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, + body == null ? "" : body); + + String signature = sign(message, "SHA256withRSA", privateKey); + + return String.format( + "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + + "timestamp=\"%d\",serial_no=\"%s\"", + mchid, nonce, signature, timestamp, certificateSerialNo); + } + + /** + * 对参数进行 URL 编码 + * + * @param content 参数内容 + * @return 编码后的内容 + */ + public static String urlEncode(String content) { + try { + return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * 对参数Map进行 URL 编码,生成 QueryString + * + * @param params Query参数Map + * @return QueryString + */ + public static String urlEncode(Map params) { + if (params == null || params.isEmpty()) { + return ""; + } + + int index = 0; + StringBuilder result = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + result.append(entry.getKey()) + .append("=") + .append(urlEncode(entry.getValue().toString())); + index++; + if (index < params.size()) { + result.append("&"); + } + } + return result.toString(); + } + + /** + * 从应答中提取 Body + * + * @param response HTTP 请求应答对象 + * @return 应答中的Body内容,Body为空时返回空字符串 + */ + public static String extractBody(Response response) { + if (response.body() == null) { + return ""; + } + + try { + BufferedSource source = response.body().source(); + return source.readUtf8(); + } catch (IOException e) { + throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); + } + } + + /** + * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 + * + * @param wechatpayPublicKeyId 微信支付公钥ID + * @param wechatpayPublicKey 微信支付公钥对象 + * @param headers 微信支付应答 Header 列表 + * @param body 微信支付应答 Body + */ + public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, + Headers headers, + String body) { + String timestamp = headers.get("Wechatpay-Timestamp"); + try { + Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); + // 拒绝过期请求 + if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { + throw new IllegalArgumentException( + String.format("Validate http response,timestamp[%s] of httpResponse is expires, " + + "request-id[%s]", + timestamp, headers.get("Request-ID"))); + } + } catch (DateTimeException | NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + + "request-id[%s]", timestamp, + headers.get("Request-ID"))); + } + String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), + body == null ? "" : body); + String serialNumber = headers.get("Wechatpay-Serial"); + if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { + throw new IllegalArgumentException( + String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, + serialNumber)); + } + String signature = headers.get("Wechatpay-Signature"); + + boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); + if (!success) { + throw new IllegalArgumentException( + String.format("Validate response failed,the WechatPay signature is incorrect.%n" + + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", + headers.get("Request-ID"), headers, body)); + } + } + + /** + * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 + */ + public static class ApiException extends RuntimeException { + public final int statusCode; + public final String body; + public final Headers headers; + public ApiException(int statusCode, String body, Headers headers) { + super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body)); + this.statusCode = statusCode; + this.body = body; + this.headers = headers; + } + } +}