fix: 客户品牌

This commit is contained in:
old burden 2026-04-01 13:09:43 +08:00
parent 2e8210ca90
commit 656e6feadd
50 changed files with 1998 additions and 238 deletions

View File

@ -2,12 +2,9 @@ package com.ruoyi.api;
import com.ruoyi.ai.domain.AiSampleAmount; import com.ruoyi.ai.domain.AiSampleAmount;
import com.ruoyi.ai.domain.AiSampleAmountRecord; import com.ruoyi.ai.domain.AiSampleAmountRecord;
import com.ruoyi.ai.domain.AiStatistics;
import com.ruoyi.ai.domain.enums.AiConfigEnum;
import com.ruoyi.ai.mapper.AiSampleAmountMapper; import com.ruoyi.ai.mapper.AiSampleAmountMapper;
import com.ruoyi.ai.service.EmailVerifyService; import com.ruoyi.ai.service.EmailVerifyService;
import com.ruoyi.ai.service.IAiSampleAmountRecordService; import com.ruoyi.ai.service.IAiSampleAmountRecordService;
import com.ruoyi.ai.service.IAiStatisticsService;
import com.ruoyi.ai.service.IAiUserService; import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.constant.BalanceChangerConstants;
@ -22,10 +19,8 @@ import com.ruoyi.common.core.domain.model.RegisterAiUserBody;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.job.TaskException; import com.ruoyi.common.exception.job.TaskException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.IpCountryQueryByApi;
import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.web.service.SysLoginService; import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.quartz.domain.SysJob; import com.ruoyi.quartz.domain.SysJob;
import com.ruoyi.quartz.service.ISysJobService; import com.ruoyi.quartz.service.ISysJobService;
@ -40,7 +35,10 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.UUID;
/** /**
@ -104,8 +102,11 @@ public class AiUserApiController extends BaseController {
// 查询启用状态体验金活动 // 查询启用状态体验金活动
AiSampleAmount aiSampleAmount = aiSampleAmountMapper.getSampleAmount(); AiSampleAmount aiSampleAmount = aiSampleAmountMapper.getSampleAmount();
if (aiSampleAmount != null) { if (aiSampleAmount != null) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
String orderNo = dateTime + uuid;
// 余额变更记录 // 余额变更记录
aiUserService.addUserBalance(aiUser.getId(), aiSampleAmount.getAmount(), BalanceChangerConstants.EXPERIENCE_GOLD_GIFT); aiUserService.addUserBalance(orderNo, aiUser.getId(), aiSampleAmount.getAmount(), BalanceChangerConstants.EXPERIENCE_GOLD_GIFT);
// 新增体验金记录 // 新增体验金记录
AiSampleAmountRecord aiSampleAmountRecord = new AiSampleAmountRecord(); AiSampleAmountRecord aiSampleAmountRecord = new AiSampleAmountRecord();
aiSampleAmountRecord.setUserId(aiUser.getId()); aiSampleAmountRecord.setUserId(aiUser.getId());
@ -218,6 +219,7 @@ public class AiUserApiController extends BaseController {
/** /**
* 查询返佣比例配置 * 查询返佣比例配置
*
* @return * @return
*/ */
@GetMapping("/getRebateConfig") @GetMapping("/getRebateConfig")

View File

@ -0,0 +1,38 @@
package com.ruoyi.api;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.TencentCosUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* COS 上传兼容接口
*/
@RestController
@RequestMapping("/api/cos")
@Api(tags = "COS文件上传")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class CosController {
private final TencentCosUtil tencentCosUtil;
@ApiOperation("COS上传接口")
@PostMapping("/upload")
public AjaxResult upload(
@ApiParam(name = "file", value = "文件", required = true)
@RequestParam("file") MultipartFile file) throws Exception {
String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
AjaxResult ajax = AjaxResult.success(uploadUrl);
ajax.put("url", uploadUrl);
ajax.put("oldName", file.getOriginalFilename());
return ajax;
}
}

View File

@ -1,7 +1,7 @@
package com.ruoyi.api; package com.ruoyi.api;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.AwsS3Util; import com.ruoyi.common.utils.TencentCosUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
@Api(tags = "文件上传") @Api(tags = "文件上传")
@RequiredArgsConstructor(onConstructor_ = @Autowired) @RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FileController { public class FileController {
private final AwsS3Util awsS3Util; private final TencentCosUtil tencentCosUtil;
/** /**
* 文件上传 * 文件上传
@ -30,7 +30,7 @@ public class FileController {
@ApiParam(name = "file", value = "文件", required = true) @ApiParam(name = "file", value = "文件", required = true)
@RequestParam("file") MultipartFile file) throws Exception { @RequestParam("file") MultipartFile file) throws Exception {
AjaxResult ajax = AjaxResult.success(); AjaxResult ajax = AjaxResult.success();
String uploadUrl = awsS3Util.uploadMultipartFile(file, true); String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
ajax.put("url", uploadUrl); ajax.put("url", uploadUrl);
return ajax; return ajax;
} }

View File

@ -69,8 +69,8 @@ public class KadaPaymentJava {
map.put("pm", "QRPH"); map.put("pm", "QRPH");
map.put("ref", orderNo); map.put("ref", orderNo);
map.put("payer", payer); map.put("payer", payer);
map.put("redirect", "www.google.com/recharge"); map.put("redirect", "https://undressing.top/recharge");
map.put("callbackUrl", "www.google.com/api/pay/kada-callBack"); map.put("callbackUrl", "https://undressing.top/api/pay/kada-callBack");
return JSON.toJSONString(map); return JSON.toJSONString(map);
} }

View File

@ -5,8 +5,10 @@ import com.ruoyi.ai.service.IAiRechargeService;
import com.ruoyi.ai.service.IJinShaService; import com.ruoyi.ai.service.IJinShaService;
import com.ruoyi.ai.service.IKaDaService; import com.ruoyi.ai.service.IKaDaService;
import com.ruoyi.ai.service.IYuZhouService; import com.ruoyi.ai.service.IYuZhouService;
import com.ruoyi.ai.service.IVmService;
import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ip.IpUtils;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -25,6 +27,7 @@ public class PayController {
private final IKaDaService kaDaService; private final IKaDaService kaDaService;
private final IAiRechargeService aiRechargeService; private final IAiRechargeService aiRechargeService;
private final IYuZhouService yuZhouService; private final IYuZhouService yuZhouService;
private final IVmService vmService;
/** /**
* jinsha请求支付 * jinsha请求支付
@ -85,4 +88,109 @@ public class PayController {
return s; return s;
} }
/**
* VM请求支付
*/
@PostMapping("/vm-pay")
@ApiOperation("VM请求支付")
public AjaxResult vmPay(@RequestBody VmPayReq req, HttpServletRequest request) throws Exception {
// 获取用户真实IP地址
String clientIp = IpUtils.getIpAddr(request);
PayResVO payResVO = vmService.vmPay(req.getGearId(), req.getVmCardInfo(), clientIp);
return AjaxResult.success(payResVO);
}
@PostMapping("/vm-callBack")
@ApiOperation("VM支付回调")
@Anonymous
public String vmCallBack(HttpServletRequest request) {
// 记录请求的Content-Type
String contentType = request.getContentType();
// 从请求中解析表单参数
VmCallBackReq req = new VmCallBackReq();
// 记录所有请求参数用于调试
java.util.Enumeration<String> paramNames = request.getParameterNames();
java.util.Map<String, String> allParams = new java.util.HashMap<>();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String paramValue = request.getParameter(paramName);
allParams.put(paramName, paramValue);
}
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PayController.class);
log.info("VM支付回调请求参数: {}, Content-Type: {}", allParams, contentType);
req.setMchNo(request.getParameter("mchNo"));
req.setAppId(request.getParameter("appId"));
req.setMchOrderNo(request.getParameter("mchOrderNo"));
req.setPayOrderId(request.getParameter("payOrderId"));
req.setWayCode(request.getParameter("wayCode"));
// 解析整数类型字段
String amountStr = request.getParameter("amount");
if (amountStr != null && !amountStr.isEmpty()) {
try {
req.setAmount(Integer.parseInt(amountStr));
} catch (NumberFormatException e) {
log.warn("VM支付回调amount解析失败: {}", amountStr);
}
}
req.setCurrency(request.getParameter("currency"));
req.setIfCode(request.getParameter("ifCode"));
req.setSubject(request.getParameter("subject"));
req.setBody(request.getParameter("body"));
req.setClientIp(request.getParameter("clientIp"));
// 解析订单状态文档字段名state
String stateStr = request.getParameter("state");
if (stateStr != null && !stateStr.isEmpty()) {
try {
req.setState(Integer.parseInt(stateStr));
} catch (NumberFormatException e) {
log.warn("VM支付回调state解析失败: {}", stateStr);
}
}
req.setChannelOrderNo(request.getParameter("channelOrderNo"));
req.setErrCode(request.getParameter("errCode"));
req.setErrMsg(request.getParameter("errMsg"));
req.setExtParam(request.getParameter("extParam"));
// 解析时间戳createdAtsuccessTimereqTime文档字段名
String createdAtStr = request.getParameter("createdAt");
if (createdAtStr != null && !createdAtStr.isEmpty()) {
try {
req.setCreatedAt(Long.parseLong(createdAtStr));
} catch (NumberFormatException e) {
log.warn("VM支付回调createdAt解析失败: {}", createdAtStr);
}
}
String successTimeStr = request.getParameter("successTime");
if (successTimeStr != null && !successTimeStr.isEmpty()) {
try {
req.setSuccessTime(Long.parseLong(successTimeStr));
} catch (NumberFormatException e) {
log.warn("VM支付回调successTime解析失败: {}", successTimeStr);
}
}
String reqTimeStr = request.getParameter("reqTime");
if (reqTimeStr != null && !reqTimeStr.isEmpty()) {
try {
req.setReqTime(Long.parseLong(reqTimeStr));
} catch (NumberFormatException e) {
log.warn("VM支付回调reqTime解析失败: {}", reqTimeStr);
}
}
req.setSign(request.getParameter("sign"));
req.setSignType(request.getParameter("signType"));
log.info("VM支付回调解析后的对象: mchOrderNo={}, state={}, sign={}",
req.getMchOrderNo(), req.getState(), req.getSign());
return vmService.vmCallBack(req);
}
} }

View File

@ -31,6 +31,11 @@ public class ByteApiRequest {
@ApiModelProperty(name = "标签字符串") @ApiModelProperty(name = "标签字符串")
private String tags; private String tags;
@ApiModelProperty(name = "使用的模型")
private String model;
@ApiModelProperty(name = "生成模式text-to-video 或 image-to-video")
private String mode = "text-to-video";
} }

View File

@ -0,0 +1,38 @@
package com.ruoyi.api.request;
import com.ruoyi.ai.domain.ContentItem;
import lombok.Data;
import java.util.List;
/**
* 门户视频生成火山 Seedance请求体
*/
@Data
public class PortalVideoGenRequest {
private String text;
/**
* 文生视频时可选多段文本 + 参考图与火山 content 一致不传则仅使用 text 单行
*/
private List<ContentItem> content;
/** 默认与后台配置的视频计费类型一致 */
private String functionType = "21";
private String model;
private Integer duration;
private String resolution;
private String ratio;
private String firstUrl;
private String lastUrl;
/** 图生视频-参考图模式 */
private String referenceUrl;
}

View File

@ -0,0 +1,120 @@
package com.ruoyi.web.controller.ai;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* AI 业务侧部门管理数据源与系统部门 sys_dept 一致权限独立
*/
@RestController
@RequestMapping("/ai/dept")
public class AiDeptController extends BaseController
{
@Autowired
private ISysDeptService deptService;
@Autowired
private IAiUserService aiUserService;
@PreAuthorize("@ss.hasPermi('ai:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
@PreAuthorize("@ss.hasPermi('ai:dept:list')")
@GetMapping("/list/exclude/{deptId}")
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
{
List<SysDept> depts = deptService.selectDeptList(new SysDept());
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
return success(depts);
}
@PreAuthorize("@ss.hasPermi('ai:dept:query')")
@GetMapping(value = "/{deptId}")
public AjaxResult getInfo(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
return success(deptService.selectDeptById(deptId));
}
@PreAuthorize("@ss.hasPermi('ai:dept:add')")
@Log(title = "AI部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}
@PreAuthorize("@ss.hasPermi('ai:dept:edit')")
@Log(title = "AI部门管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDept dept)
{
Long deptId = dept.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.checkDeptNameUnique(dept))
{
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
else if (dept.getParentId().equals(deptId))
{
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
{
return error("该部门包含未停用的子部门!");
}
dept.setUpdateBy(getUsername());
return toAjax(deptService.updateDept(dept));
}
@PreAuthorize("@ss.hasPermi('ai:dept:remove')")
@Log(title = "AI部门管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{deptId}")
public AjaxResult remove(@PathVariable Long deptId)
{
if (deptService.hasChildByDeptId(deptId))
{
return warn("存在下级部门,不允许删除");
}
if (deptService.checkDeptExistUser(deptId))
{
return warn("部门存在系统用户,不允许删除");
}
if (aiUserService.countAiUserByDeptId(deptId) > 0)
{
return warn("部门存在 AI 用户,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}
}

View File

@ -1,4 +1,4 @@
package com.ruoyi.ai.controller; package com.ruoyi.web.controller.ai;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;

View File

@ -1,33 +1,27 @@
package com.ruoyi.web.controller.ai; package com.ruoyi.web.controller.ai;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import com.ruoyi.ai.service.EmailVerifyService;
import com.ruoyi.ai.service.IAiUserService; import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.AiUser; import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.model.RegisterAiUserBody;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* ai-用户信息Controller * ai-用户信息Controller
@ -77,7 +71,6 @@ public class AiUserController extends BaseController {
} }
/** /**
* 新增ai-用户信息 * 新增ai-用户信息
*/ */
@ -100,6 +93,20 @@ public class AiUserController extends BaseController {
return toAjax(aiUserService.updateAiUser(aiUser)); return toAjax(aiUserService.updateAiUser(aiUser));
} }
/**
* 分配归属部门deptId 为空表示不归属任何部门
*/
@ApiOperation("分配AI用户归属部门")
@PreAuthorize("@ss.hasPermi('ai:user:edit')")
@Log(title = "ai-用户信息", businessType = BusinessType.UPDATE)
@PutMapping("/dept")
public AjaxResult assignDept(@RequestBody AiUser aiUser) {
if (aiUser.getId() == null) {
return error("用户主键不能为空");
}
return toAjax(aiUserService.updateAiUserDept(aiUser.getId(), aiUser.getDeptId()));
}
/** /**
* 状态修改 * 状态修改
*/ */
@ -123,9 +130,12 @@ public class AiUserController extends BaseController {
aiUser.setUpdateBy(getUsername()); aiUser.setUpdateBy(getUsername());
// 获取原余额 // 获取原余额
AiUser u = aiUserService.selectAiUserById(aiUser.getId()); AiUser u = aiUserService.selectAiUserById(aiUser.getId());
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
String orderNo = dateTime + uuid;
// 计算变更余额 // 计算变更余额
BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance()); BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance());
aiUserService.addUserBalance(aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION); aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION);
return toAjax(1); return toAjax(1);
} }
@ -151,8 +161,6 @@ public class AiUserController extends BaseController {
} }
// @GetMapping("/addUserBalance") // @GetMapping("/addUserBalance")
// public AjaxResult addUserBalance(BigDecimal amount) { // public AjaxResult addUserBalance(BigDecimal amount) {
// aiUserService.addUserBalance(SecurityUtils.getAiUserId(), amount, BalanceChangerConstants.RECHARGE); // aiUserService.addUserBalance(SecurityUtils.getAiUserId(), amount, BalanceChangerConstants.RECHARGE);

View File

@ -4,7 +4,6 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.AwsS3Util;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.TencentCosUtil; import com.ruoyi.common.utils.TencentCosUtil;
import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUploadUtils;
@ -36,7 +35,6 @@ public class CommonController {
private static final Logger log = LoggerFactory.getLogger(CommonController.class); private static final Logger log = LoggerFactory.getLogger(CommonController.class);
private final ServerConfig serverConfig; private final ServerConfig serverConfig;
private final TencentCosUtil tencentCosUtil; private final TencentCosUtil tencentCosUtil;
private final AwsS3Util awsS3Util;
/** /**
* 通用下载请求 * 通用下载请求
@ -158,7 +156,7 @@ public class CommonController {
} }
/** /**
* AWS上传请求单个 * 腾讯云COS上传请求单个
*/ */
@ApiOperation("图片上传接口") @ApiOperation("图片上传接口")
@PostMapping("/aws/upload") @PostMapping("/aws/upload")
@ -166,7 +164,7 @@ public class CommonController {
AjaxResult ajax = AjaxResult.success(); AjaxResult ajax = AjaxResult.success();
String uploadUrl; String uploadUrl;
try { try {
uploadUrl = awsS3Util.uploadMultipartFile(file, true); uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
} catch (Exception e) { } catch (Exception e) {
return AjaxResult.error(e.getMessage()); return AjaxResult.error(e.getMessage());
} }

View File

@ -5,9 +5,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://xxxxx:xxxxx/byteai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://database-1.cvs822qoc391.ap-southeast-1.rds.amazonaws.com:3306/byteai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: root
password: xxxxxx password: mkMReisAKl6I7rVqEY90
driverClassName: com.mysql.cj.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver
# 初始连接数 # 初始连接数
@ -41,8 +41,8 @@ spring:
allow: allow:
url-pattern: /druid/* url-pattern: /druid/*
# 控制台管理用户名和密码 # 控制台管理用户名和密码
login-username: login-username: ruoyi
login-password: login-password: cvs822qoc391
filter: filter:
stat: stat:
enabled: true enabled: true
@ -54,3 +54,4 @@ spring:
config: config:
multi-statement-allow: true multi-statement-allow: true

View File

@ -71,6 +71,12 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
</dependency> </dependency>
<!-- Apache Commons Codec for Base64 encoding/decoding -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- excel工具 --> <!-- excel工具 -->
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>

View File

@ -117,12 +117,19 @@ public class AiUser extends BaseEntity {
*/ */
private String country; private String country;
/** 归属部门IDsys_dept.dept_id */
private Long deptId;
/** /**
* 上级用户昵称 * 上级用户昵称
*/ */
@TableField(exist = false) @TableField(exist = false)
private String superiorName; private String superiorName;
/** 归属部门名称 */
@TableField(exist = false)
private String deptName;
/** /**
* 上级用户ID * 上级用户ID
*/ */

View File

@ -52,6 +52,9 @@ public class SysDept extends BaseEntity
/** 父部门名称 */ /** 父部门名称 */
private String parentName; private String parentName;
/** Byte API Key */
private String byteApiKey;
/** 子部门 */ /** 子部门 */
private List<SysDept> children = new ArrayList<SysDept>(); private List<SysDept> children = new ArrayList<SysDept>();
@ -171,6 +174,16 @@ public class SysDept extends BaseEntity
this.parentName = parentName; this.parentName = parentName;
} }
public String getByteApiKey()
{
return byteApiKey;
}
public void setByteApiKey(String byteApiKey)
{
this.byteApiKey = byteApiKey;
}
public List<SysDept> getChildren() public List<SysDept> getChildren()
{ {
return children; return children;
@ -192,6 +205,7 @@ public class SysDept extends BaseEntity
.append("leader", getLeader()) .append("leader", getLeader())
.append("phone", getPhone()) .append("phone", getPhone())
.append("email", getEmail()) .append("email", getEmail())
.append("byteApiKey", getByteApiKey())
.append("status", getStatus()) .append("status", getStatus())
.append("delFlag", getDelFlag()) .append("delFlag", getDelFlag())
.append("createBy", getCreateBy()) .append("createBy", getCreateBy())

View File

@ -13,7 +13,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID; import java.util.UUID;
@Component @Component
@ -33,22 +39,32 @@ public class TencentCosUtil {
private String domain; private String domain;
//文件上传 /**
public String upload(MultipartFile file) { * 上传MultipartFile到腾讯云COS返回文件访问地址
* 与AwsS3Util.uploadMultipartFile方法接口兼容
*/
public String upload(MultipartFile file) throws Exception {
return uploadMultipartFile(file, true);
}
/**
* 上传MultipartFile到腾讯云COS返回文件访问地址
*
* @param file 前端上传的MultipartFile
* @param isPublic 是否公开访问当前实现中忽略使用domain配置
* @return 文件访问地址URL字符串
*/
public String uploadMultipartFile(MultipartFile file, boolean isPublic) throws Exception {
if (file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 3 生成 cos 客户端
COSClient cosClient = createCosClient(); COSClient cosClient = createCosClient();
// 存储桶的命名格式为 BucketName-APPID此处填写的存储桶名称必须为此格式 // 生成唯一文件键格式与AWS一致yyyy/MM/dd/uuid_filename
// 对象键(Key)是对象在存储桶中的唯一标识 998u-09iu-09i-333 String key = generateCosKey(file.getOriginalFilename());
//在文件名称前面添加uuid值
String key = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8) + "_"
+ file.getOriginalFilename();
//对上传文件分组根据当前日期 /2022/11/11
String dateTime = new DateTime().toString("yyyy/MM/dd");
key = dateTime + "/" + key;
try { try {
//获取上传文件输入流
InputStream inputStream = file.getInputStream(); InputStream inputStream = file.getInputStream();
ObjectMetadata objectMetadata = new ObjectMetadata(); ObjectMetadata objectMetadata = new ObjectMetadata();
PutObjectRequest putObjectRequest = new PutObjectRequest( PutObjectRequest putObjectRequest = new PutObjectRequest(
@ -56,17 +72,41 @@ public class TencentCosUtil {
key, key,
inputStream, inputStream,
objectMetadata); objectMetadata);
// 高级接口会返回一个异步结果Upload
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest); PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
//返回上传文件路径 // 返回COS文件访问地址
//https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg String url = domain + (domain.endsWith("/") ? "" : "/") + key;
String url = domain + "/" + key;
return url; return url;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException("上传文件到COS失败: " + e.getMessage(), e);
} finally {
cosClient.shutdown();
}
}
/**
* 通过URL下载文件并上传到COS
* 与AwsS3Util.uploadFileByUrl方法接口兼容
*/
public String uploadFileByUrl(String fileUrl) throws Exception {
return uploadFileByUrl(fileUrl, true);
}
public String uploadFileByUrl(String fileUrl, boolean isPublic) throws Exception {
if (fileUrl == null || fileUrl.trim().isEmpty()) {
throw new IllegalArgumentException("文件下载链接不能为空");
}
Path tempPath = downloadFileToTemp(fileUrl);
try {
// 使用临时文件上传
MultipartFile multipartFile = createMultipartFileFromPath(tempPath, extractFileNameFromUrl(fileUrl));
return uploadMultipartFile(multipartFile, isPublic);
} finally {
Files.deleteIfExists(tempPath);
} }
return null;
} }
@ -78,7 +118,107 @@ public class TencentCosUtil {
ClientConfig clientConfig = new ClientConfig(region); ClientConfig clientConfig = new ClientConfig(region);
// 这里建议设置使用 https 协议 // 这里建议设置使用 https 协议
clientConfig.setHttpProtocol(HttpProtocol.https); clientConfig.setHttpProtocol(HttpProtocol.https);
clientConfig.setConnectionTimeout(30 * 1000); // 连接超时30秒
clientConfig.setSocketTimeout(60 * 1000); // 读取超时60秒
//1.3 生成cos客户端 //1.3 生成cos客户端
return new COSClient(credentials, clientConfig); return new COSClient(credentials, clientConfig);
} }
/**
* 生成COS文件键与AWS保持一致的命名格式
*/
private String generateCosKey(String originalFileName) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new DateTime().toString("yyyy/MM/dd");
return dateTime + "/" + uuid + "_" + originalFileName;
}
/**
* 下载文件到临时路径
*/
private Path downloadFileToTemp(String fileUrl) throws Exception {
String suffix = getFileSuffixFromUrl(fileUrl);
Path tempPath = Files.createTempFile("url-upload-", suffix);
HttpURLConnection connection = null;
InputStream in = null;
OutputStream out = null;
try {
URL url = new URL(fileUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new RuntimeException("文件下载失败,状态码:" + responseCode);
}
in = connection.getInputStream();
out = Files.newOutputStream(tempPath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} finally {
if (out != null) {
try { out.close(); } catch (IOException e) { e.printStackTrace(); }
}
if (in != null) {
try { in.close(); } catch (IOException e) { e.printStackTrace(); }
}
if (connection != null) {
connection.disconnect();
}
}
return tempPath;
}
private String extractFileNameFromUrl(String fileUrl) {
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
if (fileName.contains("?")) {
fileName = fileName.split("\\?")[0];
}
return fileName.isEmpty() ? "default_file" : fileName;
}
private String getFileSuffixFromUrl(String fileUrl) {
String fileName = extractFileNameFromUrl(fileUrl);
if (fileName.contains(".")) {
return fileName.substring(fileName.lastIndexOf("."));
}
return ".tmp";
}
/**
* 将Path转换为MultipartFile简单实现用于uploadFileByUrl
*/
private MultipartFile createMultipartFileFromPath(Path path, String originalFilename) throws IOException {
byte[] bytes = Files.readAllBytes(path);
return new MultipartFile() {
@Override
public String getName() { return "file"; }
@Override
public String getOriginalFilename() { return originalFilename; }
@Override
public String getContentType() { return null; }
@Override
public boolean isEmpty() { return bytes.length == 0; }
@Override
public long getSize() { return bytes.length; }
@Override
public byte[] getBytes() throws IOException { return bytes; }
@Override
public InputStream getInputStream() throws IOException { return Files.newInputStream(path); }
@Override
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
Files.copy(path, dest.toPath());
}
};
}
} }

View File

@ -1,14 +1,19 @@
package com.ruoyi.common.utils.sign; package com.ruoyi.common.utils.sign;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
@ -16,25 +21,29 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@Slf4j
public class RSAUtils { public class RSAUtils {
// RSA最加密明 // RSA最大加密明文大小
private static final int MAX_ENCRYPT_BLOCK = 117; private static final int MAX_ENCRYPT_BLOCK = 117;
// 不仅可以使DSA算法同样也可以使RSA算法做数字签名 // 不仅可以使用DSA算法同样也可以使用RSA算法做数字签名
private static final String KEY_ALGORITHM = "RSA"; private static final String KEY_ALGORITHM = "RSA";
public static String encryptByPrivateKey(String str, String privateKey) throws Exception { public static String encryptByPrivateKey(String str, String privateKey)
throws InvalidKeySpecException, NoSuchAlgorithmException, javax.crypto.NoSuchPaddingException, java.security.InvalidKeyException,
javax.crypto.IllegalBlockSizeException, javax.crypto.BadPaddingException, UnsupportedEncodingException {
// base64编码的公钥 // base64编码的公钥
byte[] keyBytes = decryptBASE64(privateKey); byte[] keyBytes = decryptBASE64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM)
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
// RSA加密 // RSA加密
Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, priKey); cipher.init(Cipher.ENCRYPT_MODE, priKey);
byte[] data = str.getBytes("UTF-8"); byte[] data = str.getBytes("UTF-8");
// 加密时超过117字节就报错为此采分段加密的办法来加密 // 加密时超过117字节就报错为此采分段加密的办法来加密
byte[] enBytes = null; byte[] enBytes = null;
for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) { for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) {
// 注意要使2的倍数否则会出现加密后的内容再解密时为乱码 // 注意要使2的倍数否则会出现加密后的内容再解密时为乱码
byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK)); byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK));
enBytes = ArrayUtils.addAll(enBytes, doFinal); enBytes = ArrayUtils.addAll(enBytes, doFinal);
} }
@ -43,15 +52,22 @@ public class RSAUtils {
} }
private static String encryptBASE64(byte[] data) { private static String encryptBASE64(byte[] data) {
return new String(Base64.encode(data)); return new String(Base64.encodeBase64(data));
} }
private static byte[] decryptBASE64(String data) { private static byte[] decryptBASE64(String data) {
return Base64.decode(data); return Base64.decodeBase64(data);
} }
/**
* Verify signature
*
* @param params
* @return
*/
public static boolean verifySign(JSONObject params, String publickey) { public static boolean verifySign(JSONObject params, String publickey) {
String platSign = params.getStr("signature"); // sign String platSign = params.getStr("signature"); // sign
log.info("signature:" + platSign);
List<String> paramNameList = new ArrayList<>(); List<String> paramNameList = new ArrayList<>();
for (String key : params.keySet()) { for (String key : params.keySet()) {
if (!"signature".equals(key)) { if (!"signature".equals(key)) {
@ -64,13 +80,14 @@ public class RSAUtils {
String name = paramNameList.get(i); String name = paramNameList.get(i);
stringBuilder.append(params.getStr(name)); stringBuilder.append(params.getStr(name));
} }
log.info("keys:" + stringBuilder);
String decryptSign = ""; String decryptSign = "";
try { try {
decryptSign = publicDecrypt(platSign, getPublicKey(publickey) decryptSign = publicDecrypt(platSign, getPublicKey(publickey));
);
} catch (Exception e) { } catch (Exception e) {
System.out.println(e.toString()); log.error("Error decrypting signature", e);
} }
log.info("decryptSign:" + decryptSign);
if (!stringBuilder.toString().equalsIgnoreCase(decryptSign)) { if (!stringBuilder.toString().equalsIgnoreCase(decryptSign)) {
return false; return false;
} }
@ -89,11 +106,12 @@ public class RSAUtils {
stringBuilder.append(createMap.get(key)); // 拼接参数 stringBuilder.append(createMap.get(key)); // 拼接参数
} }
String keyStr = stringBuilder.toString(); // 得到待加密的字符串 String keyStr = stringBuilder.toString(); // 得到待加密的字符串
log.info("keyStr:" + keyStr);
String signedStr = ""; String signedStr = "";
try { try {
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密 signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
} catch (Exception e) { } catch (Exception e) {
System.out.println(e.toString()); log.error("Error encrypting signature", e);
} }
return signedStr; return signedStr;
} }
@ -110,47 +128,70 @@ public class RSAUtils {
stringBuilder.append(createMap.get(key)); // 拼接参数 stringBuilder.append(createMap.get(key)); // 拼接参数
} }
String keyStr = stringBuilder.toString(); // 得到待加密的字符串 String keyStr = stringBuilder.toString(); // 得到待加密的字符串
log.info("keyStr:" + keyStr);
String signedStr = ""; String signedStr = "";
try { try {
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密 signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
} catch (Exception e) { } catch (Exception e) {
System.out.println(e.toString()); log.error("Error encrypting signature", e);
} }
return signedStr; return signedStr;
} }
/**
* private key encryption
*
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) { public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try { try {
Cipher cipher = Cipher.getInstance("RSA"); Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey); cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encode(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes("UTF-8"), privateKey.getModulus().bitLength())); return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes("UTF-8"), privateKey.getModulus().bitLength()));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Exception encountered while encrypting string [" + data + "]", e); throw new RuntimeException("Exception encountered while encrypting string [" + data + "]", e);
} }
} }
/**
* public key decryption
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) { public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try { try {
Cipher cipher = Cipher.getInstance("RSA"); Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey); cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decode(data), publicKey.getModulus().bitLength()), "UTF-8"); return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), "UTF-8");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("An exception was encountered while decrypting the string[" + data + "]", e); throw new RuntimeException("An exception was encountered while decrypting the string[" + data + "]", e);
} }
} }
public static RSAPrivateKey getPrivateKey(String privateKey) throws Exception { /**
* get private key
*
* @param privateKey key string (base64 encoded)
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//Get the private key object through the PKCS#8 encoded Key instruction //Get the private key object through the PKCS#8 encoded Key instruction
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec); RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key; return key;
} }
public static RSAPublicKey getPublicKey(String publicKey) throws Exception { /**
* get the public key
*
* @param publicKey key string (base64 encoded)
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//Get the public key object through the X509 encoded Key instruction //Get the public key object through the X509 encoded Key instruction
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decode(publicKey)); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec); RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key; return key;
} }

View File

@ -1,16 +1,14 @@
package com.ruoyi.ai.domain; package com.ruoyi.ai.domain;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal;
/** /**
* 余额使用记录对象 ai_balance_change_record * 余额使用记录对象 ai_balance_change_record
@ -24,13 +22,23 @@ public class AiBalanceChangeRecord extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /**
* 主键ID
*/
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
/** 删除标志0代表存在 2代表删除 */ /**
* 删除标志0代表存在 2代表删除
*/
private String delFlag; private String delFlag;
/**
* 关联订单号
*/
@Excel(name = "关联订单号")
private String orderNo;
/** /**
* 用户ID * 用户ID
*/ */
@ -42,19 +50,27 @@ public class AiBalanceChangeRecord extends BaseEntity {
@TableField(exist = false) @TableField(exist = false)
private String uuid; private String uuid;
/** 操作类型0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作 */ /**
* 操作类型0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作
*/
@Excel(name = "操作类型") @Excel(name = "操作类型")
private Integer type; private Integer type;
/** 变更金额 */ /**
* 变更金额
*/
@Excel(name = "变更金额") @Excel(name = "变更金额")
private BigDecimal changeAmount; private BigDecimal changeAmount;
/** 变更后金额 */ /**
* 变更后金额
*/
@Excel(name = "变更后金额") @Excel(name = "变更后金额")
private BigDecimal resultAmount; private BigDecimal resultAmount;
/** 用户昵称 */ /**
* 用户昵称
*/
@TableField(exist = false) @TableField(exist = false)
@Excel(name = "用户昵称") @Excel(name = "用户昵称")
private String nickname; private String nickname;

View File

@ -68,6 +68,28 @@ public class AiOrder extends BaseEntity {
@Excel(name = "是否置顶N-否 Y-是") @Excel(name = "是否置顶N-否 Y-是")
private String isTop; private String isTop;
/** 生成模式text-to-video 或 image-to-video */
@Excel(name = "生成模式")
private String mode;
/** 视频时长(秒) */
@Excel(name = "视频时长")
private Integer duration;
/** 分辨率(如 720p, 1080p */
@Excel(name = "分辨率")
private String resolution;
/** 宽高比(如 16:9, 9:16 */
@Excel(name = "宽高比")
private String ratio;
/** 火山/方舟模型 endpoint */
private String model;
/** 视频类订单:本次提交的完整参数 JSON提示词、模型、时长等 */
private String videoParams;
/** 首帧图片 */ /** 首帧图片 */
private String img1; private String img1;

View File

@ -1,16 +1,15 @@
package com.ruoyi.ai.domain; package com.ruoyi.ai.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/** /**
* 返佣记录对象 ai_rebate_record * 返佣记录对象 ai_rebate_record
* *
@ -23,22 +22,38 @@ public class AiRebateRecord extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /**
* 主键ID
*/
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
/** 删除标志0代表存在 2代表删除 */ /**
* 关联订单号
*/
@Excel(name = "关联订单号")
private String orderNo;
/**
* 删除标志0代表存在 2代表删除
*/
private String delFlag; private String delFlag;
/** 上级ID */ /**
* 上级ID
*/
@Excel(name = "上级ID") @Excel(name = "上级ID")
private Long superiorId; private Long superiorId;
/** 下级ID */ /**
* 下级ID
*/
@Excel(name = "下级ID") @Excel(name = "下级ID")
private Long subordinateId; private Long subordinateId;
/** 返佣金额 */ /**
* 返佣金额
*/
@Excel(name = "返佣金额") @Excel(name = "返佣金额")
private BigDecimal amount; private BigDecimal amount;

View File

@ -1,18 +1,17 @@
package com.ruoyi.ai.domain; package com.ruoyi.ai.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/** /**
* 体验金领取记录对象 ai_sample_amount_record * 体验金领取记录对象 ai_sample_amount_record
* *
@ -25,22 +24,38 @@ public class AiSampleAmountRecord extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /**
* 主键ID
*/
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
/** 删除标志0代表存在 2代表删除 */ /**
* 关联订单号
*/
@Excel(name = "关联订单号")
private String orderNo;
/**
* 删除标志0代表存在 2代表删除
*/
private String delFlag; private String delFlag;
/** 用户ID */ /**
* 用户ID
*/
@Excel(name = "用户ID") @Excel(name = "用户ID")
private Long userId; private Long userId;
/** 体验金额 */ /**
* 体验金额
*/
@Excel(name = "体验金额") @Excel(name = "体验金额")
private BigDecimal sampleAmount; private BigDecimal sampleAmount;
/** 回收时间 */ /**
* 回收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "回收时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @Excel(name = "回收时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date recycleTime; private Date recycleTime;

View File

@ -43,6 +43,10 @@ public class AiTemplate extends BaseEntity {
@Excel(name = "模版图片URL") @Excel(name = "模版图片URL")
private String imageUrl; private String imageUrl;
/** AI类型 */
@Excel(name = "AI类型")
private Long aiId;
/** 状态0-禁用1-启用 */ /** 状态0-禁用1-启用 */
@Excel(name = "状态0-禁用1-启用") @Excel(name = "状态0-禁用1-启用")
private Integer status; private Integer status;

View File

@ -30,5 +30,10 @@ public class ByteBodyReq {
@JsonProperty("content") @JsonProperty("content")
private List<ContentItem> content; private List<ContentItem> content;
private Integer duration;
private String resolution;
private String ratio;
private Integer seed;
} }

View File

@ -15,6 +15,12 @@ public class ContentItem {
@JsonProperty("image_url") @JsonProperty("image_url")
private ImageUrl imageUrl; private ImageUrl imageUrl;
@JsonProperty("video_url")
private ImageUrl videoUrl;
@JsonProperty("audio_url")
private ImageUrl audioUrl;
@JsonProperty("role") @JsonProperty("role")
private String role; private String role;
} }

View File

@ -21,9 +21,9 @@ public class JinShaBodyReq {
private String notify_url; private String notify_url;
private String return_url;
private String sign; private String sign;
private String cardname;
private String cardno;
/** /**
* 档位ID * 档位ID

View File

@ -0,0 +1,119 @@
package com.ruoyi.ai.domain;
import lombok.Data;
/**
* VM支付回调请求
*
* @author system
* @date 2025-01-XX
*/
@Data
public class VmCallBackReq {
/**
* 商户号
*/
private String mchNo;
/**
* 应用ID
*/
private String appId;
/**
* 商户订单号
*/
private String mchOrderNo;
/**
* 支付订单号
*/
private String payOrderId;
/**
* 支付方式
*/
private String wayCode;
/**
* 支付金额单位分
*/
private Integer amount;
/**
* 货币代码
*/
private String currency;
/**
* 支付接口编码
*/
private String ifCode;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述
*/
private String body;
/**
* 订单状态0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已撤销, 5-已退款, 6-订单关闭
* 文档字段名state
*/
private Integer state;
/**
* 客户端IP可选
*/
private String clientIp;
/**
* 渠道订单号可选
*/
private String channelOrderNo;
/**
* 渠道错误码可选
*/
private String errCode;
/**
* 渠道错误描述可选
*/
private String errMsg;
/**
* 扩展参数回调时会原样返回
*/
private String extParam;
/**
* 订单创建时间13位时间戳
*/
private Long createdAt;
/**
* 订单支付成功时间13位时间戳可选
*/
private Long successTime;
/**
* 通知请求时间13位时间戳文档字段名reqTime
*/
private Long reqTime;
/**
* 签名值
*/
private String sign;
/**
* 签名类型文档回调示例无此字段不参与签名
*/
private String signType;
}

View File

@ -0,0 +1,54 @@
package com.ruoyi.ai.domain;
import lombok.Data;
/**
* VM卡信息
* 当wayCode为BUZHI_VM时需要在extParam中传入此信息的JSON字符串
*
* @author system
* @date 2025-01-XX
*/
@Data
public class VmCardInfo {
/**
* 信用卡卡号
*/
private String number;
/**
* CVC
*/
private String cvc;
/**
* 过期年四位数字2027
*/
private String expYear;
/**
* 过期月两位数11
*/
private String expMonth;
/**
* 信用卡持有者电子邮件
*/
private String email;
/**
* 持卡人名字
*/
private String firstName;
/**
* 持卡人姓氏
*/
private String lastName;
/**
* 国家: 双字母 ISO 国家代码两位字母
*/
private String country;
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.ai.domain;
import lombok.Data;
/**
* VM支付请求
*
* @author system
* @date 2025-01-XX
*/
@Data
public class VmPayReq {
/**
* 档位ID
*/
private Long gearId;
/**
* VM卡信息可选如果为null则需要在extParam中传入
*/
private VmCardInfo vmCardInfo;
}

View File

@ -0,0 +1,99 @@
package com.ruoyi.ai.domain;
import lombok.Data;
/**
* VM统一下单请求
*
* @author system
* @date 2025-01-XX
*/
@Data
public class VmUnifiedOrderReq {
/**
* 商户号
*/
private String mchNo;
/**
* 应用ID
*/
private String appId;
/**
* 商户订单号
*/
private String mchOrderNo;
/**
* 支付方式BUZHI_VM国际VM卡支付
*/
private String wayCode;
/**
* 支付金额单位分
*/
private Integer amount;
/**
* 货币代码三位货币代码人民币:cny 美元USD
*/
private String currency;
/**
* 客户端IPV4地址
*/
private String clientIp;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述
*/
private String body;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 订单失效时间单位秒默认2小时
*/
private Integer expiredTime;
/**
* 特定渠道发起的额外参数json格式字符串
*/
private String channelExtra;
/**
* 商户扩展参数回调时会原样返回
* 当wayCode为BUZHI_VM时需要传入VM卡信息的JSON字符串
*/
private String extParam;
/**
* 请求接口时间13位时间戳
*/
private Long reqTime;
/**
* 接口版本号固定1.0
*/
private String version;
/**
* 签名值
*/
private String sign;
/**
* 签名类型目前只支持MD5方式
*/
private String signType;
}

View File

@ -0,0 +1,100 @@
package com.ruoyi.ai.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* VM统一下单响应
*
* @author system
* @date 2025-01-XX
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class VmUnifiedOrderRes {
/**
* 响应码0表示成功
*/
private Integer code;
/**
* 响应消息
*/
private String msg;
/**
* 响应数据
*/
private VmUnifiedOrderData data;
/**
* 响应签名API 返回
*/
private String sign;
@Data
public static class VmUnifiedOrderData {
/**
* 支付订单号必填
*/
private String payOrderId;
/**
* 商户订单号必填
*/
private String mchOrderNo;
/**
* 订单状态必填
* 0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已撤销, 5-已退款, 6-订单关闭
*/
private Integer orderState;
/**
* 支付数据类型必填
* payUrl-跳转链接的方式
* form-表单方式
* wxapp-微信支付参数(微信公众号,小程序,app支付时)
* aliapp-支付宝app支付参数
* ysfapp-云闪付app支付参数
* codeUrl-二维码地址
* codeImgUrl-二维码图片地址
* none-空支付参数
*/
private String payDataType;
/**
* 支付数据可选
* 发起支付用到的支付参数根据payDataType不同而不同
* 如果payDataType为payUrl则此字段为跳转链接
*/
private String payData;
/**
* 渠道错误码可选
*/
private String errCode;
/**
* 渠道错误描述可选
*/
private String errMsg;
// 以下字段为兼容旧版本保留
/**
* 支付链接兼容字段优先使用payData
*/
private String payUrl;
/**
* 订单号兼容字段
*/
private String orderId;
/**
* 支付参数兼容字段
*/
private String payParams;
}
}

View File

@ -22,5 +22,8 @@ public interface AiOrderMapper extends BaseMapper<AiOrder> {
AiOrder getAiOrderByResult(@Param("result") String result); AiOrder getAiOrderByResult(@Param("result") String result);
/** 门户视频:按火山任务 id 查单result 可能已被替换为成品 URL故兼查 video_params.volcTaskId */
AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId);
BigDecimal getSumAmountByUserId(@Param("userId") String userId); BigDecimal getSumAmountByUserId(@Param("userId") String userId);
} }

View File

@ -6,6 +6,7 @@ import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.domain.entity.AiUser; import com.ruoyi.common.core.domain.entity.AiUser;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/** /**
* ai-用户信息Mapper接口 * ai-用户信息Mapper接口
@ -22,4 +23,9 @@ public interface AiUserMapper extends BaseMapper<AiUser> {
AiUser selectAiUserById(Long id); AiUser selectAiUserById(Long id);
AiUser getUserInfo(Long id); AiUser getUserInfo(Long id);
int countAiUserByDeptId(@Param("deptId") Long deptId);
@Update("update ai_user set dept_id = #{deptId}, update_time = sysdate() where id = #{userId}")
int updateAiUserDeptId(@Param("userId") Long userId, @Param("deptId") Long deptId);
} }

View File

@ -79,5 +79,7 @@ public interface IAiOrderService {
AiOrder getAiOrderByResult(String result); AiOrder getAiOrderByResult(String result);
AiOrder getAiOrderByPortalVideoTask(String taskId);
BigDecimal getSumAmountByUserId(String userId); BigDecimal getSumAmountByUserId(String userId);
} }

View File

@ -87,9 +87,9 @@ public interface IAiUserService {
void updateEmail(String email, String code); void updateEmail(String email, String code);
void addUserBalance(Long userId, BigDecimal amount, int recharge); void addUserBalance(String orderNo, Long userId, BigDecimal amount, int recharge);
void addUserBalance(Long userId, BigDecimal amount, int recharge, String remark); void addUserBalance(String orderNo, Long userId, BigDecimal amount, int recharge, String remark);
AiUser getUserByEmail(String email); AiUser getUserByEmail(String email);
@ -99,9 +99,19 @@ public interface IAiUserService {
int updatePassword(AiUser aiUser); int updatePassword(AiUser aiUser);
/**
* 部门下 AI 用户数量用于删除部门前校验
*/
int countAiUserByDeptId(Long deptId);
/**
* 分配 AI 用户归属部门deptId 为空则清空
*/
int updateAiUserDept(Long userId, Long deptId);
public void deductSampleAmount(String userId); public void deductSampleAmount(String userId);
void handleRebate(Long userId, BigDecimal rechargeAmount); void handleRebate(String orderNo, Long userId, BigDecimal rechargeAmount);
String getRebateConfig(); String getRebateConfig();

View File

@ -0,0 +1,12 @@
package com.ruoyi.ai.service;
/**
* 门户用户火山方舟 API Key取自所属二级部门 {@code sys_dept.byte_api_key} Redis 缓存
*/
public interface IByteDeptApiKeyService {
/**
* Redis {userId}_byte_api_key优先读缓存再读库均无有效 key 则抛出业务异常
*/
String resolveVolcApiKey(Long aiUserId);
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.ByteBodyReq; import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes; import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.common.core.domain.AjaxResult;
public interface IByteService { public interface IByteService {
@ -16,12 +17,31 @@ public interface IByteService {
ByteBodyRes imgToImg(ByteBodyReq req) throws Exception; ByteBodyRes imgToImg(ByteBodyReq req) throws Exception;
/** /**
* 首尾帧图生视频 * 首尾帧图生视频使用全局配置的 Ark API Key
*/ */
ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception; ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception;
/**
* 视频生成任务创建指定 Ark API Key门户按部门密钥调用
*/
ByteBodyRes imgToVideo(ByteBodyReq req, String arkApiKey) throws Exception;
/** /**
* 下载视频 * 下载视频
*/ */
ByteBodyRes uploadVideo(String id) throws Exception; ByteBodyRes uploadVideo(String id) throws Exception;
ByteBodyRes uploadVideo(String id, String arkApiKey) throws Exception;
/**
* 取消视频生成任务
*/
AjaxResult cancelVideoTask(String id) throws Exception;
AjaxResult cancelVideoTask(String id, String arkApiKey) throws Exception;
/**
* GET 查询视频生成任务列表火山 list 文档返回原始 JSON 字符串
*/
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
} }

View File

@ -0,0 +1,33 @@
package com.ruoyi.ai.service;
import java.math.BigDecimal;
/**
* 汇率服务接口
*
* @author system
* @date 2025-01-XX
*/
public interface IExchangeRateService {
/**
* 获取汇率
*
* @param fromCurrency 源货币代码CNY
* @param toCurrency 目标货币代码PHP
* @return 汇率值
* @throws Exception 获取汇率失败时抛出异常
*/
BigDecimal getExchangeRate(String fromCurrency, String toCurrency) throws Exception;
/**
* 转换金额
*
* @param amount 原始金额
* @param fromCurrency 源货币代码CNY
* @param toCurrency 目标货币代码PHP
* @return 转换后的金额
* @throws Exception 获取汇率失败时抛出异常
*/
BigDecimal convertAmount(BigDecimal amount, String fromCurrency, String toCurrency) throws Exception;
}

View File

@ -8,6 +8,7 @@ public interface IJinShaService {
/** /**
* 支付 * 支付
* @param gearId 档位ID
*/ */
PayResVO jinShaPay(Long gearId) throws Exception; PayResVO jinShaPay(Long gearId) throws Exception;

View File

@ -0,0 +1,32 @@
package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.PayResVO;
import com.ruoyi.ai.domain.VmCallBackReq;
/**
* VM支付服务接口
*
* @author system
* @date 2025-01-XX
*/
public interface IVmService {
/**
* VM支付
*
* @param gearId 档位ID
* @param vmCardInfo VM卡信息可选如果为null则需要在extParam中传入
* @param clientIp 客户端IP地址
* @return 支付结果
* @throws Exception 支付异常
*/
PayResVO vmPay(Long gearId, com.ruoyi.ai.domain.VmCardInfo vmCardInfo, String clientIp) throws Exception;
/**
* VM支付回调文档要求返回字符串 "success" 表示成功 success 表示失败支付中心将再次通知
*
* @param req 回调请求
* @return "success" 表示处理成功其他字符串表示失败
*/
String vmCallBack(VmCallBackReq req);
}

View File

@ -1,29 +1,24 @@
package com.ruoyi.ai.service.impl; package com.ruoyi.ai.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ai.domain.*;
import com.ruoyi.ai.mapper.AiRechargeMapper;
import com.ruoyi.ai.service.*;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.utils.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ai.domain.*;
import com.ruoyi.ai.domain.enums.AiConfigEnum;
import com.ruoyi.ai.service.*;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.domain.SysConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ai.mapper.AiRechargeMapper;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 充值管理Service业务层处理 * 充值管理Service业务层处理
* *
@ -84,6 +79,10 @@ public class AiRechargeServiceImpl implements IAiRechargeService {
public void addRecharge(String orderNo) { public void addRecharge(String orderNo) {
// 设置到账时间 // 设置到账时间
AiRecharge aiRecharge = this.getAiRechargeByOrderNum(orderNo); AiRecharge aiRecharge = this.getAiRechargeByOrderNum(orderNo);
if (aiRecharge == null) {
log.error("支付回调订单不存在orderNo: {}", orderNo);
return;
}
Date creditedTime = aiRecharge.getCreditedTime(); Date creditedTime = aiRecharge.getCreditedTime();
if (creditedTime != null) { if (creditedTime != null) {
log.error("支付回调重复 {}", orderNo); log.error("支付回调重复 {}", orderNo);
@ -107,12 +106,10 @@ public class AiRechargeServiceImpl implements IAiRechargeService {
aiRecharge.setGiftAmount(amount); aiRecharge.setGiftAmount(amount);
// 新增用户余额 // 新增用户余额
aiUserService.addUserBalance(aiRecharge.getUserId(), amount, BalanceChangerConstants.RECHARGE); aiUserService.addUserBalance(orderNo, aiRecharge.getUserId(), amount, BalanceChangerConstants.RECHARGE);
// 处理返佣 (根据充值金额计算返佣) // 处理返佣 (根据充值金额计算返佣)
aiUserService.handleRebate(aiRecharge.getUserId(), aiRechargeGiftGear.getRechargeAmount()); aiUserService.handleRebate(orderNo, aiRecharge.getUserId(), aiRechargeGiftGear.getRechargeAmount());
// 新增充值统计 // 新增充值统计
AiStatistics aiStatistics = new AiStatistics(); AiStatistics aiStatistics = new AiStatistics();
aiStatistics.setSource(aiRecharge.getSource()); aiStatistics.setSource(aiRecharge.getSource());
@ -127,7 +124,7 @@ public class AiRechargeServiceImpl implements IAiRechargeService {
if (isAvailable) { if (isAvailable) {
aiRecharge.setGiveAmount(aiRechargeGiftGear.getGiveAmount()); aiRecharge.setGiveAmount(aiRechargeGiftGear.getGiveAmount());
// 新增用户赠送余额 // 新增用户赠送余额
aiUserService.addUserBalance(aiRecharge.getUserId(), aiRechargeGiftGear.getGiveAmount(), BalanceChangerConstants.RECHARGE_BONUS); aiUserService.addUserBalance(orderNo, aiRecharge.getUserId(), aiRechargeGiftGear.getGiveAmount(), BalanceChangerConstants.RECHARGE_BONUS);
// 新增充值赠送记录 // 新增充值赠送记录
AiRechargeGiftRecord aiRechargeGiftRecord = new AiRechargeGiftRecord(); AiRechargeGiftRecord aiRechargeGiftRecord = new AiRechargeGiftRecord();
aiRechargeGiftRecord.setOrderNum(orderNo); aiRechargeGiftRecord.setOrderNum(orderNo);

View File

@ -9,20 +9,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ai.domain.*; import com.ruoyi.ai.domain.*;
import com.ruoyi.ai.domain.enums.AiConfigEnum; import com.ruoyi.ai.domain.enums.AiConfigEnum;
import com.ruoyi.ai.mapper.AiRechargeMapper; import com.ruoyi.ai.mapper.AiRechargeMapper;
import com.ruoyi.ai.mapper.AiSampleAmountMapper; import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.ai.mapper.AiSampleAmountRecordMapper;
import com.ruoyi.ai.service.*; import com.ruoyi.ai.service.*;
import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.entity.AiUser; import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.common.core.domain.model.LoginAiUser; import com.ruoyi.common.core.domain.model.LoginAiUser;
import com.ruoyi.common.core.domain.model.LoginAiUserBody; import com.ruoyi.common.core.domain.model.LoginAiUserBody;
import com.ruoyi.common.core.domain.model.RegisterAiUserBody; import com.ruoyi.common.core.domain.model.RegisterAiUserBody;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.*; import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.uuid.UUID;
import com.ruoyi.system.domain.SysConfig; import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper; import com.ruoyi.system.mapper.SysConfigMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -30,7 +27,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -70,7 +66,6 @@ public class AiUserServiceImpl implements IAiUserService {
private AiRechargeMapper aiRechargeMapper; private AiRechargeMapper aiRechargeMapper;
/** /**
* 查询ai-用户信息 * 查询ai-用户信息
* *
@ -137,6 +132,19 @@ public class AiUserServiceImpl implements IAiUserService {
return aiUserMapper.updateById(aiUser); return aiUserMapper.updateById(aiUser);
} }
@Override
public int countAiUserByDeptId(Long deptId) {
if (deptId == null) {
return 0;
}
return aiUserMapper.countAiUserByDeptId(deptId);
}
@Override
public int updateAiUserDept(Long userId, Long deptId) {
return aiUserMapper.updateAiUserDeptId(userId, deptId);
}
/** /**
* 批量删除ai-用户信息 * 批量删除ai-用户信息
* *
@ -208,7 +216,7 @@ public class AiUserServiceImpl implements IAiUserService {
// 新增访客统计 // 新增访客统计
AiStatistics aiStatistics = new AiStatistics(); AiStatistics aiStatistics = new AiStatistics();
aiStatistics.setSource(registerAiUserBody.getSource()); aiStatistics.setSource(registerAiUserBody.getSource());
aiStatistics.setAddUserCount(1l); aiStatistics.setAddUserCount(1L);
aiStatisticsService.saveOrUpdateData(aiStatistics); aiStatisticsService.saveOrUpdateData(aiStatistics);
return aiUser; return aiUser;
} }
@ -352,13 +360,13 @@ public class AiUserServiceImpl implements IAiUserService {
} }
@Override @Override
public void addUserBalance(Long userId, BigDecimal amount, int recharge) { public void addUserBalance(String orderNo, Long userId, BigDecimal amount, int recharge) {
addUserBalance(userId, amount, recharge, null); addUserBalance(orderNo, userId, amount, recharge, null);
} }
@Override @Override
@Transactional @Transactional
public void addUserBalance(Long userId, BigDecimal amount, int recharge, String remark) { public void addUserBalance(String orderNo, Long userId, BigDecimal amount, int recharge, String remark) {
// 新增余额使用记录 // 新增余额使用记录
AiUser aiUser = aiUserMapper.selectById(userId); AiUser aiUser = aiUserMapper.selectById(userId);
AiBalanceChangeRecord balanceChangeRecord = new AiBalanceChangeRecord(); AiBalanceChangeRecord balanceChangeRecord = new AiBalanceChangeRecord();
@ -367,6 +375,7 @@ public class AiUserServiceImpl implements IAiUserService {
balanceChangeRecord.setChangeAmount(amount); balanceChangeRecord.setChangeAmount(amount);
balanceChangeRecord.setResultAmount(NumberUtil.add(amount, aiUser.getBalance())); balanceChangeRecord.setResultAmount(NumberUtil.add(amount, aiUser.getBalance()));
balanceChangeRecord.setRemark(remark); balanceChangeRecord.setRemark(remark);
balanceChangeRecord.setOrderNo(orderNo);
balanceChangeRecordService.insertAiBalanceChangeRecord(balanceChangeRecord); balanceChangeRecordService.insertAiBalanceChangeRecord(balanceChangeRecord);
// 修改用户余额 // 修改用户余额
aiUser.setBalance(balanceChangeRecord.getResultAmount()); aiUser.setBalance(balanceChangeRecord.getResultAmount());
@ -410,14 +419,14 @@ public class AiUserServiceImpl implements IAiUserService {
// if (deductAmount.compareTo(BigDecimal.ZERO) < 0) { // if (deductAmount.compareTo(BigDecimal.ZERO) < 0) {
// deductAmount = new BigDecimal(0); // deductAmount = new BigDecimal(0);
// } // }
addUserBalance(Long.valueOf(userId), NumberUtil.mul(aiSampleAmountRecord.getSampleAmount(), -1), BalanceChangerConstants.EXPERIENCE_GOLD_RECYCLE); addUserBalance(aiSampleAmountRecord.getOrderNo(), Long.valueOf(userId), NumberUtil.mul(aiSampleAmountRecord.getSampleAmount(), -1), BalanceChangerConstants.EXPERIENCE_GOLD_RECYCLE);
// 修改状态已回收 // 修改状态已回收
aiSampleAmountRecord.setStatus(1); aiSampleAmountRecord.setStatus(1);
aiSampleAmountRecordService.updateAiSampleAmountRecord(aiSampleAmountRecord); aiSampleAmountRecordService.updateAiSampleAmountRecord(aiSampleAmountRecord);
} }
@Override @Override
public void handleRebate(Long userId, BigDecimal amount) { public void handleRebate(String orderNo, Long userId, BigDecimal amount) {
AiUser aiUser = aiUserMapper.selectById(userId); AiUser aiUser = aiUserMapper.selectById(userId);
if (aiUser.getSuperiorId() == null) return; if (aiUser.getSuperiorId() == null) return;
// 查询上级用户 // 查询上级用户
@ -428,7 +437,7 @@ public class AiUserServiceImpl implements IAiUserService {
SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique(AiConfigEnum.SYS_INVITE_COMMISSION.getValue()); SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique(AiConfigEnum.SYS_INVITE_COMMISSION.getValue());
double decimal = NumberUtil.div(Integer.parseInt(sysConfig.getConfigValue()), 100, 2); double decimal = NumberUtil.div(Integer.parseInt(sysConfig.getConfigValue()), 100, 2);
BigDecimal rebateAmount = NumberUtil.mul(amount, decimal); BigDecimal rebateAmount = NumberUtil.mul(amount, decimal);
addUserBalance(superiorUser.getId(), rebateAmount, BalanceChangerConstants.REBATE); addUserBalance(orderNo, superiorUser.getId(), rebateAmount, BalanceChangerConstants.REBATE);
// 新增返佣记录 // 新增返佣记录
AiRebateRecord rebateRecord = new AiRebateRecord(); AiRebateRecord rebateRecord = new AiRebateRecord();
rebateRecord.setSuperiorId(superiorUser.getId()); rebateRecord.setSuperiorId(superiorUser.getId());

View File

@ -0,0 +1,99 @@
package com.ruoyi.ai.service.impl;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
private static final String NO_DEPT_MSG = "用户未分配部门:请在后台为门户用户设置 ai_user.dept_id关联 sys_dept.dept_id";
private static final String NO_DEPT_ROW_MSG = "用户所属部门不存在或已删除,请核对 ai_user.dept_id";
private static final String NO_API_KEY_MSG = "部门未配置火山 API Key请在 sys_dept 为用户所在部门(或其上级二级部门)配置 byte_api_key";
private static final int CACHE_HOURS = 1;
@Autowired
private RedisCache redisCache;
@Autowired
private IAiUserService aiUserService;
@Autowired
private ISysDeptService sysDeptService;
@Override
public String resolveVolcApiKey(Long aiUserId) {
if (aiUserId == null) {
throw new ServiceException(NO_DEPT_MSG);
}
String cacheKey = aiUserId + "_byte_api_key";
String cached = redisCache.getCacheObject(cacheKey);
if (StringUtils.isNotEmpty(cached)) {
return cached;
}
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
if (aiUser == null || aiUser.getDeptId() == null) {
throw new ServiceException(NO_DEPT_MSG);
}
SysDept userDept = sysDeptService.selectDeptById(aiUser.getDeptId());
if (userDept == null) {
throw new ServiceException(NO_DEPT_ROW_MSG);
}
// 优先使用用户直接归属部门的 Key多数业务把用户挂在分公司 101并在该节点配 byte_api_key
String apiKey = trimKey(userDept.getByteApiKey());
if (StringUtils.isEmpty(apiKey)) {
Long fallbackDeptId = resolveSecondLevelDeptId(userDept);
if (!fallbackDeptId.equals(userDept.getDeptId())) {
SysDept keyDept = sysDeptService.selectDeptById(fallbackDeptId);
if (keyDept != null) {
apiKey = trimKey(keyDept.getByteApiKey());
}
}
}
if (StringUtils.isEmpty(apiKey)) {
throw new ServiceException(NO_API_KEY_MSG);
}
redisCache.setCacheObject(cacheKey, apiKey, CACHE_HOURS, TimeUnit.HOURS);
return apiKey;
}
private static String trimKey(String raw) {
return raw == null ? null : raw.trim();
}
/**
* 二级部门祖级路径中紧接在一级ancestors 第二段之下的部门节点
* 深度不足时退回用户当前部门
*/
private Long resolveSecondLevelDeptId(SysDept userDept) {
String ancestors = userDept.getAncestors();
if (StringUtils.isEmpty(ancestors)) {
return userDept.getDeptId();
}
String[] parts = ancestors.split(",");
if (parts.length >= 3) {
try {
return Long.parseLong(parts[2].trim());
} catch (NumberFormatException ignored) {
return userDept.getDeptId();
}
}
if (parts.length == 2) {
try {
return Long.parseLong(parts[1].trim());
} catch (NumberFormatException ignored) {
return userDept.getDeptId();
}
}
return userDept.getDeptId();
}
}

View File

@ -0,0 +1,292 @@
package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.ai.service.IExchangeRateService;
import com.ruoyi.common.utils.http.OkHttpUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 汇率服务实现类
*
* @author system
* @date 2025-01-XX
*/
@Service
@Slf4j
public class ExchangeRateServiceImpl implements IExchangeRateService {
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${exchange-rate.api-url:https://api.currencyfreaks.com/v2.0/rates/latest}")
private String apiUrl;
@Value("${exchange-rate.apikey:}")
private String apikey;
@Value("${exchange-rate.base-currency:USD}")
private String baseCurrency;
@Value("${exchange-rate.enabled:true}")
private boolean enabled;
@Value("${exchange-rate.fallback-rate-jinsha:90}")
private BigDecimal fallbackRateJinSha;
@Value("${exchange-rate.fallback-rate-kada:60}")
private BigDecimal fallbackRateKaDa;
@Value("${exchange-rate.fallback-rate-php:90}")
private BigDecimal fallbackRatePHP;
@Override
public BigDecimal getExchangeRate(String fromCurrency, String toCurrency) throws Exception {
if (!enabled) {
log.warn("汇率服务未启用,使用默认汇率");
return getFallbackRate(toCurrency);
}
try {
// 构建请求URL - 使用 CurrencyFreaks Rates Latest Endpoint
String url = buildApiUrl();
// 构建请求
Request request = new Request.Builder()
.url(url)
.get()
.build();
// 发送请求
Response response = OkHttpUtils.newCall(request).execute();
ResponseBody body = response.body();
if (response.isSuccessful() && body != null) {
String responseBody = body.string();
log.debug("汇率API响应: {}", responseBody);
// 解析响应 - CurrencyFreaks Rates Latest Endpoint 响应格式
RatesResponse ratesResponse = objectMapper.readValue(responseBody, RatesResponse.class);
if (ratesResponse != null && ratesResponse.getRates() != null) {
BigDecimal rate = calculateRate(ratesResponse, fromCurrency, toCurrency);
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
log.info("成功获取汇率 {} -> {}: {}", fromCurrency, toCurrency, rate);
return rate;
}
}
} else {
log.warn("汇率API请求失败状态码: {}", response.code());
if (body != null) {
log.warn("响应内容: {}", body.string());
}
}
} catch (Exception e) {
log.error("获取汇率失败,使用备用汇率: {}", e.getMessage(), e);
}
// 如果API调用失败使用备用汇率
return getFallbackRate(toCurrency);
}
@Override
public BigDecimal convertAmount(BigDecimal amount, String fromCurrency, String toCurrency) throws Exception {
if (amount == null) {
throw new IllegalArgumentException("金额不能为空");
}
if (!enabled) {
log.warn("汇率服务未启用,使用默认汇率计算");
BigDecimal rate = getFallbackRate(toCurrency);
// 返回完整精度不限制小数位
return amount.multiply(rate);
}
try {
// 构建请求URL - 使用 CurrencyFreaks Rates Latest Endpoint
String url = buildApiUrl();
// 构建请求
Request request = new Request.Builder()
.url(url)
.get()
.build();
// 发送请求
Response response = OkHttpUtils.newCall(request).execute();
ResponseBody body = response.body();
if (response.isSuccessful() && body != null) {
String responseBody = body.string();
// log.debug("汇率API响应: {}", responseBody);
// 解析响应 - CurrencyFreaks Rates Latest Endpoint 响应格式
RatesResponse ratesResponse = objectMapper.readValue(responseBody, RatesResponse.class);
if (ratesResponse != null && ratesResponse.getRates() != null) {
BigDecimal rate = calculateRate(ratesResponse, fromCurrency, toCurrency);
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal convertedAmount = amount.multiply(rate);
log.info("成功转换金额 {} {} -> {} {}: {}",
amount, fromCurrency, convertedAmount, toCurrency, rate);
// 返回完整精度不限制小数位
return convertedAmount;
}
}
} else {
log.warn("汇率API请求失败状态码: {}", response.code());
if (body != null) {
log.warn("响应内容: {}", body.string());
}
}
} catch (Exception e) {
log.error("转换金额失败,使用备用汇率计算: {}", e.getMessage(), e);
}
// 如果API调用失败使用备用汇率计算不限制小数位
BigDecimal rate = getFallbackRate(toCurrency);
return amount.multiply(rate);
}
/**
* 构建API URL - 使用 CurrencyFreaks Rates Latest Endpoint
*/
private String buildApiUrl() {
if (apiUrl.contains("currencyfreaks.com") && apiUrl.contains("/rates/latest")) {
// 使用 CurrencyFreaks Rates Latest Endpoint 格式
// https://api.currencyfreaks.com/v2.0/rates/latest?apikey=YOUR_APIKEY&base=USD
StringBuilder urlBuilder = new StringBuilder(apiUrl);
urlBuilder.append("?apikey=").append(apikey);
urlBuilder.append("&base=").append(baseCurrency.toUpperCase());
return urlBuilder.toString();
} else {
// 兼容其他API格式向后兼容
return apiUrl;
}
}
/**
* 根据汇率响应计算 fromCurrency toCurrency 的汇率
*/
private BigDecimal calculateRate(RatesResponse ratesResponse, String fromCurrency, String toCurrency) {
String base = ratesResponse.getBase();
java.util.Map<String, String> rates = ratesResponse.getRates();
if (rates == null || rates.isEmpty()) {
log.warn("汇率响应中 rates 为空");
return null;
}
String fromUpper = fromCurrency.toUpperCase();
String toUpper = toCurrency.toUpperCase();
String baseUpper = base.toUpperCase();
// 如果源货币就是基准货币
if (baseUpper.equals(fromUpper)) {
String toRateStr = rates.get(toUpper);
if (toRateStr != null) {
try {
return new BigDecimal(toRateStr);
} catch (NumberFormatException e) {
log.error("无法解析目标货币 {} 的汇率: {}", toUpper, toRateStr);
return null;
}
} else {
log.warn("未找到目标货币 {} 的汇率", toUpper);
return null;
}
}
// 如果目标货币就是基准货币
if (baseUpper.equals(toUpper)) {
String fromRateStr = rates.get(fromUpper);
if (fromRateStr != null) {
try {
// 如果目标货币是基准货币汇率是 1 / fromRate
BigDecimal fromRate = new BigDecimal(fromRateStr);
if (fromRate.compareTo(BigDecimal.ZERO) > 0) {
return BigDecimal.ONE.divide(fromRate, 10, RoundingMode.HALF_UP);
}
} catch (NumberFormatException e) {
log.error("无法解析源货币 {} 的汇率: {}", fromUpper, fromRateStr);
return null;
}
} else {
log.warn("未找到源货币 {} 的汇率", fromUpper);
return null;
}
}
// 两个货币都不是基准货币需要计算
String fromRateStr = rates.get(fromUpper);
String toRateStr = rates.get(toUpper);
if (fromRateStr == null) {
log.warn("未找到源货币 {} 的汇率", fromUpper);
return null;
}
if (toRateStr == null) {
log.warn("未找到目标货币 {} 的汇率", toUpper);
return null;
}
try {
BigDecimal fromRate = new BigDecimal(fromRateStr);
BigDecimal toRate = new BigDecimal(toRateStr);
if (fromRate.compareTo(BigDecimal.ZERO) > 0) {
// 汇率 = toRate / fromRate
// 例如base=USD, from=CNY, to=PHP
// USD->CNY = fromRate, USD->PHP = toRate
// CNY->PHP = toRate / fromRate
return toRate.divide(fromRate, 10, RoundingMode.HALF_UP);
}
} catch (NumberFormatException e) {
log.error("无法解析汇率: from={}, to={}", fromRateStr, toRateStr);
return null;
}
return null;
}
/**
* 获取备用汇率当API调用失败时使用
*/
private BigDecimal getFallbackRate(String toCurrency) {
// 根据目标货币返回不同的备用汇率
if ("PHP".equalsIgnoreCase(toCurrency)) {
// PHP比索使用配置的PHP备用汇率
log.warn("使用PHP备用汇率: {}", fallbackRatePHP);
return fallbackRatePHP;
}
// 其他货币可以扩展默认使用PHP的备用汇率
log.warn("未配置货币 {} 的备用汇率使用PHP默认汇率 {}", toCurrency, fallbackRatePHP);
return fallbackRatePHP;
}
/**
* CurrencyFreaks Rates Latest Endpoint 响应对象
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class RatesResponse {
@JsonProperty("date")
private String date;
@JsonProperty("base")
private String base;
@JsonProperty("rates")
private java.util.Map<String, String> rates;
}
}

View File

@ -47,6 +47,10 @@ public class JinShaService implements IJinShaService {
private IAiUserService aiUserService; private IAiUserService aiUserService;
@Autowired @Autowired
private IAiRechargeGiftGearService aiRechargeGiftGearService; private IAiRechargeGiftGearService aiRechargeGiftGearService;
@Autowired
private IAiRechargeGiftService aiRechargeGiftService;
@Autowired
private IExchangeRateService exchangeRateService;
@Override @Override
public PayResVO jinShaPay(Long gearId) throws Exception { public PayResVO jinShaPay(Long gearId) throws Exception {
@ -57,64 +61,104 @@ public class JinShaService implements IJinShaService {
throw new ServiceException("The gear position does not exist.", -1); throw new ServiceException("The gear position does not exist.", -1);
} }
BigDecimal amount = aiRechargeGiftGear.getRechargeAmount(); BigDecimal amount = aiRechargeGiftGear.getRechargeAmount();
amount = amount.multiply(new BigDecimal(90)); // 使用汇率服务转换金额从CNY转换为目标货币默认INR
amount = exchangeRateService.convertAmount(amount, "USD", "INR");
JinShaBodyReq req = new JinShaBodyReq(); // 生成订单号
req.setAppid(appId);
req.setReturn_url(returnUrl + "/recharge");
req.setNotify_url(notifyUrl + "/api/pay/jinsha-callBack");
req.setAmount(amount);
// 1. 配置请求参数
TreeMap<String, Object> params = new TreeMap<>();
params.put("appid", req.getAppid());
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
String orderNo = dateTime + "js" + uuid; String orderNo = dateTime + "js" + uuid;
params.put("orderno", orderNo);
params.put("amount", amount); // 金额转换为number类型保留两位小数
params.put("notify_url", req.getNotify_url()); String amountStr = String.format("%.2f", amount.doubleValue());
params.put("return_url", req.getReturn_url());
StringBuilder append = this.append(params); // 构建回调地址和返回地址
String sign = this.sign(append); String notifyUrlFull = notifyUrl + "/api/pay/jinsha-callBack";
String formData = append + "&sign=" + sign; String returnUrlFull = returnUrl + "/recharge";
// 2. 构建请求体form-urlencoded 格式
RequestBody requestBody = RequestBody.create( // 1. 配置签名参数按照key排序appid, amount, orderno, notify_url, return_url不包含sign
MediaType.get("application/x-www-form-urlencoded;charset=utf-8"), TreeMap<String, Object> signParams = new TreeMap<>();
formData signParams.put("appid", appId);
); signParams.put("amount", amountStr);
// 3. 构建 POST 请求 signParams.put("orderno", orderNo);
signParams.put("notify_url", notifyUrlFull);
signParams.put("return_url", returnUrlFull);
// 2. 生成签名
String sign = this.generateSign(signParams);
// 3. 构建请求参数使用FormBody.Builder自动处理URL编码
FormBody.Builder formBuilder = new FormBody.Builder();
formBuilder.add("appid", appId);
formBuilder.add("amount", amountStr);
formBuilder.add("orderno", orderNo);
formBuilder.add("notify_url", notifyUrlFull);
formBuilder.add("return_url", returnUrlFull);
formBuilder.add("sign", sign);
RequestBody requestBody = formBuilder.build();
// 4. 构建 POST 请求
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url + "/api/pay") .url(url + "/api/pay")
// .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
.post(requestBody) .post(requestBody)
.build(); .build();
// 4. 发送请求并获取响应
// 5. 发送请求并获取响应
Response response = OkHttpUtils.newCall(request).execute(); Response response = OkHttpUtils.newCall(request).execute();
ResponseBody body = response.body(); ResponseBody body = response.body();
if (null == body) { if (null == body) {
log.error("kada支付请求的响应异常"); log.error("jinsha支付请求的响应异常");
throw new Exception("kadapay responsebody is null"); throw new Exception("jinshapay responsebody is null");
} }
JinShaBodyRes jinShaBodyRes = objectMapper.readValue(body.string(), JinShaBodyRes.class); String responseBody = body.string();
log.info("jinsha支付请求响应: {}", responseBody);
JinShaBodyRes jinShaBodyRes = objectMapper.readValue(responseBody, JinShaBodyRes.class);
// 检查响应code0表示成功其他值表示失败
Integer code = jinShaBodyRes.getCode();
if (code == null || code != 0) {
String msg = jinShaBodyRes.getMsg();
log.error("jinsha支付请求失败code: {}, msg: {}", code, msg);
throw new ServiceException(msg != null ? msg : "支付请求失败", code != null ? code : -1);
}
// 检查返回的payurl
String payUrl = null;
if (jinShaBodyRes.getData() != null) {
payUrl = jinShaBodyRes.getData().getPayurl();
}
if (payUrl == null || payUrl.trim().isEmpty()) {
log.error("jinsha支付返回的payurl为空订单号: {}", orderNo);
throw new ServiceException("支付返回的支付链接为空", -1);
}
// 创建充值管理 // 创建充值管理
AiUser userInfo = aiUserService.getUserInfo(SecurityUtils.getAiUserId()); AiUser userInfo = aiUserService.getUserInfo(SecurityUtils.getAiUserId());
AiRecharge aiRecharge = new AiRecharge(); AiRecharge aiRecharge = new AiRecharge();
aiRecharge.setOrderNum(orderNo); aiRecharge.setOrderNum(orderNo);
aiRecharge.setUserId(SecurityUtils.getAiUserId()); aiRecharge.setUserId(SecurityUtils.getAiUserId());
aiRecharge.setAmount(amount); aiRecharge.setAmount(amount);
aiRecharge.setGearId(req.getGearId()); aiRecharge.setGearId(gearId);
aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount()); aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount());
aiRecharge.setSource(userInfo.getSource()); aiRecharge.setSource(userInfo.getSource());
AiRechargeGift aiRechargeGift = aiRechargeGiftService.selectAiRechargeGiftById(aiRechargeGiftGear.getRechargeId());
aiRecharge.setPayType(aiRechargeGift.getPayType());
aiRecharge.setPayUrl(payUrl);
aiRechargeService.insertAiRecharge(aiRecharge); aiRechargeService.insertAiRecharge(aiRecharge);
PayResVO payResVO = new PayResVO(); PayResVO payResVO = new PayResVO();
payResVO.setOrderNo(orderNo); payResVO.setOrderNo(orderNo);
payResVO.setPayUrl(jinShaBodyRes.getData().getPayurl()); payResVO.setPayUrl(payUrl);
return payResVO; return payResVO;
} }
@Override @Override
@Transactional @Transactional
public AjaxResult jinShaCall(JinShaBodyCall req) { public AjaxResult jinShaCall(JinShaBodyCall req) {
log.info("jinsha支付回调订单号: {}, 状态: {}", req.getOrderno(), req.getStatus());
// 验证签名
TreeMap<String, Object> params = new TreeMap<>(); TreeMap<String, Object> params = new TreeMap<>();
params.put("status", req.getStatus()); params.put("status", req.getStatus());
params.put("orderno", req.getOrderno()); params.put("orderno", req.getOrderno());
@ -123,14 +167,58 @@ public class JinShaService implements IJinShaService {
StringBuilder append = this.append(params); StringBuilder append = this.append(params);
String sign = this.sign(append); String sign = this.sign(append);
if (!sign.equals(req.getSign())) { if (!sign.equals(req.getSign())) {
log.error("支付回调签名错误 {}", req.getOrderno()); log.error("jinsha支付回调签名错误订单号: {}, 期望签名: {}, 实际签名: {}",
return AjaxResult.error(); req.getOrderno(), sign, req.getSign());
} return AjaxResult.error("签名验证失败");
//充值成功处理
aiRechargeService.addRecharge(req.getOrderno());
return AjaxResult.success();
} }
// 处理订单状态
// status:0=>处理中, status:1=>成功, status:2=>失败
Integer status = req.getStatus();
if (status == null) {
log.error("jinsha支付回调状态为空订单号: {}", req.getOrderno());
return AjaxResult.error("状态为空");
}
if (status == 1) {
// 充值成功处理
log.info("jinsha支付成功订单号: {}", req.getOrderno());
aiRechargeService.addRecharge(req.getOrderno());
return AjaxResult.success();
} else if (status == 2) {
// 支付失败
log.warn("jinsha支付失败订单号: {}", req.getOrderno());
// 这里可以添加失败处理逻辑比如更新订单状态为失败
return AjaxResult.success();
} else {
// 处理中status:0
log.info("jinsha支付处理中订单号: {}", req.getOrderno());
return AjaxResult.success();
}
}
/**
* 生成签名
* 按照文档要求key=value&key=value&...&secret=secret然后MD5
*/
private String generateSign(TreeMap<String, Object> sortedParams) {
StringBuilder paramSb = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// 拼接格式key=value&
paramSb.append(key).append("=").append(value).append("&");
}
// 最后添加secret
paramSb.append("secret=").append(secret);
String signSource = paramSb.toString();
log.debug("jinsha签名源字符串: {}", signSource);
return Md5Utils.hash(signSource);
}
/**
* 旧的签名方法回调使用
*/
private StringBuilder append(TreeMap<String, Object> sortedParams) { private StringBuilder append(TreeMap<String, Object> sortedParams) {
StringBuilder paramSb = new StringBuilder(); StringBuilder paramSb = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) { for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {

View File

@ -54,6 +54,8 @@ public class KaDaService implements IKaDaService {
private IAiRechargeService aiRechargeService; private IAiRechargeService aiRechargeService;
@Autowired @Autowired
private IAiRechargeGiftService aiRechargeGiftService; private IAiRechargeGiftService aiRechargeGiftService;
@Autowired
private IExchangeRateService exchangeRateService;
@Override @Override
@ -65,7 +67,8 @@ public class KaDaService implements IKaDaService {
throw new ServiceException("The gear position does not exist.", -1); throw new ServiceException("The gear position does not exist.", -1);
} }
BigDecimal amount = aiRechargeGiftGear.getRechargeAmount(); BigDecimal amount = aiRechargeGiftGear.getRechargeAmount();
amount = amount.multiply(new BigDecimal(60.00)); // 使用汇率服务转换金额从CNY转换为目标货币默认PHP
amount = exchangeRateService.convertAmount(amount, "USD", "PHP");
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());

View File

@ -60,37 +60,111 @@ public class YuZhouService implements IYuZhouService {
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
String orderNo = dateTime + "yz" + uuid; String orderNo = dateTime + "yz" + uuid;
// 获取用户信息
AiUser userInfo = aiUserService.getUserInfo(SecurityUtils.getAiUserId());
// 处理用户姓名如果没有则使用默认值
String firstName = "John";
String lastName = "Doe";
if (userInfo.getNickname() != null && !userInfo.getNickname().trim().isEmpty()) {
String[] nameParts = userInfo.getNickname().trim().split("\\s+");
if (nameParts.length >= 2) {
firstName = nameParts[0];
lastName = nameParts[nameParts.length - 1];
} else if (nameParts.length == 1) {
firstName = nameParts[0];
}
}
// 从用户表获取邮箱
String email = userInfo.getEmail();
if (email == null || email.trim().isEmpty()) {
log.warn("用户邮箱为空 userId: {}, nickname: {}", userInfo.getId(), userInfo.getNickname());
// 如果用户邮箱为空使用默认邮箱格式
email = "user" + userInfo.getId() + "@example.com";
} else {
email = email.trim();
}
// 处理电话如果没有则使用默认值
String phone = userInfo.getPhone();
if (phone == null || phone.trim().isEmpty()) {
phone = "1234567890";
} else {
// 确保电话是10位数字
phone = phone.replaceAll("[^0-9]", "");
if (phone.length() < 10) {
// 如果不足10位用0补齐
phone = String.format("%010d", phone.length() > 0 ? Long.parseLong(phone) : 0);
} else if (phone.length() > 10) {
// 如果超过10位取后10位
phone = phone.substring(phone.length() - 10);
}
}
// 构建请求参数
Map<String, Object> createMap = new HashMap<>(); Map<String, Object> createMap = new HashMap<>();
createMap.put("merchantNo", appId); createMap.put("merchantNo", appId);
createMap.put("orderNo", orderNo); createMap.put("orderNo", orderNo);
createMap.put("money", amount.toString()); createMap.put("money", amount.toString());
createMap.put("description", "test"); createMap.put("description", "Recharge");
createMap.put("name", "test"); createMap.put("email", email);
createMap.put("email", "test@gmail.com"); createMap.put("phone", phone);
createMap.put("callbackUrl", callbackUrl + "/api/pay/yuzhou-callBack"); createMap.put("notifyUrl", callbackUrl + "/api/pay/yuzhou-callBack");
createMap.put("phone", "7383442114");
createMap.put("expiredPeriod", "1440"); createMap.put("expiredPeriod", "1440");
createMap.put("redirectUrl", redirectUrl + "/recharge"); createMap.put("currency", "USD");
createMap.put("firstName", firstName);
createMap.put("lastName", lastName);
createMap.put("street", "123 Main Street");
createMap.put("city", "New York");
createMap.put("state", "NY");
createMap.put("country", userInfo.getCountry() != null && !userInfo.getCountry().trim().isEmpty() ? userInfo.getCountry() : "US");
createMap.put("postcode", "10001");
// 生成签名注意签名时不包括signature字段
String signedStr = RSAUtils.getSignStr(createMap, secretKey); String signedStr = RSAUtils.getSignStr(createMap, secretKey);
createMap.put("signature", signedStr); createMap.put("signature", signedStr);
String postStr = JSONUtil.toJsonStr(createMap); String postStr = JSONUtil.toJsonStr(createMap);
String response = HttpRequest.post(url + "/gateway/order/US/payIn").header("Content-Type", "application/json") log.info("yuzhou支付请求参数: {}", postStr);
.body(postStr).execute().body();
// 调用新的接口地址
String response = HttpRequest.post(url + "/gateway/US/payIn")
.header("Content-Type", "application/json; charset=utf-8")
.body(postStr)
.execute()
.body();
log.info("yuzhou支付响应: {}", response);
JSONObject returnObj = JSONUtil.parseObj(response); JSONObject returnObj = JSONUtil.parseObj(response);
Integer code = returnObj.getInt("code"); Integer code = returnObj.getInt("code");
if (200 != code) { if (200 != code) {
log.error("yuzhou支付请求的响应异常 {}", returnObj); String msg = returnObj.getStr("msg");
throw new Exception("yuzhoupay responsebody is null"); log.error("yuzhou支付请求的响应异常 code: {}, msg: {}, response: {}", code, msg, returnObj);
throw new ServiceException("yuzhoupay request failed: " + (msg != null ? msg : "unknown error"), code);
} }
JSONObject data = returnObj.getJSONObject("data"); JSONObject data = returnObj.getJSONObject("data");
if (data == null) {
log.error("yuzhou支付响应数据为空: {}", returnObj);
throw new ServiceException("yuzhoupay response data is null", -1);
}
String money = data.getStr("money"); String money = data.getStr("money");
String payUrl = data.getStr("url"); String payUrl = data.getStr("url");
String platformOrderNo = data.getStr("platformOrderNo");
if (payUrl == null || payUrl.trim().isEmpty()) {
log.error("yuzhou支付URL为空: {}", data);
throw new ServiceException("yuzhoupay url is null", -1);
}
// 创建充值管理 // 创建充值管理
AiUser userInfo = aiUserService.getUserInfo(SecurityUtils.getAiUserId());
AiRecharge aiRecharge = new AiRecharge(); AiRecharge aiRecharge = new AiRecharge();
aiRecharge.setOrderNum(orderNo); aiRecharge.setOrderNum(orderNo);
aiRecharge.setUserId(SecurityUtils.getAiUserId()); aiRecharge.setUserId(SecurityUtils.getAiUserId());
aiRecharge.setAmount(new BigDecimal(money)); aiRecharge.setAmount(new BigDecimal(money != null ? money : amount.toString()));
aiRecharge.setGearId(gearId); aiRecharge.setGearId(gearId);
aiRecharge.setSource(userInfo.getSource()); aiRecharge.setSource(userInfo.getSource());
aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount()); aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount());
@ -98,6 +172,7 @@ public class YuZhouService implements IYuZhouService {
aiRecharge.setPayType(aiRechargeGift.getPayType()); aiRecharge.setPayType(aiRechargeGift.getPayType());
aiRecharge.setPayUrl(payUrl); aiRecharge.setPayUrl(payUrl);
aiRechargeService.insertAiRecharge(aiRecharge); aiRechargeService.insertAiRecharge(aiRecharge);
PayResVO payResVO = new PayResVO(); PayResVO payResVO = new PayResVO();
payResVO.setOrderNo(orderNo); payResVO.setOrderNo(orderNo);
payResVO.setPayUrl(payUrl); payResVO.setPayUrl(payUrl);
@ -106,31 +181,60 @@ public class YuZhouService implements IYuZhouService {
@Override @Override
public String callBack(Map<String, Object> map) throws Exception { public String callBack(Map<String, Object> map) throws Exception {
int code = Integer.parseInt(map.get("status").toString()); log.info("yuzhou支付回调接收参数: {}", map);
String orderNo = map.get("orderNo").toString();
if (10 != code) { // 验证必要参数
log.error("yuzhou支付失败 {}", orderNo); if (map == null || map.isEmpty()) {
return null; log.error("yuzhou支付回调参数为空");
return "FAIL";
} }
// Map<String, Object> createMap = new HashMap<>();
// createMap.put("orderNo", orderNo); Object statusObj = map.get("status");
// createMap.put("platOrderNo", map.get("platOrderNo")); Object orderNoObj = map.get("orderNo");
// createMap.put("money", map.get("money"));
// createMap.put("fee", map.get("fee")); if (statusObj == null || orderNoObj == null) {
// createMap.put("status", map.get("status")); log.error("yuzhou支付回调缺少必要参数: status={}, orderNo={}", statusObj, orderNoObj);
// createMap.put("message", map.get("message")); return "FAIL";
String signature = map.get("signature").toString(); }
int status = Integer.parseInt(statusObj.toString());
String orderNo = orderNoObj.toString();
log.info("yuzhou支付回调 orderNo: {}, status: {}", orderNo, status);
// 验证签名
Object signatureObj = map.get("signature");
if (signatureObj == null) {
log.error("yuzhou支付回调缺少签名参数 orderNo: {}", orderNo);
return "FAIL";
}
JSONObject entries = JSONUtil.parseObj(map); JSONObject entries = JSONUtil.parseObj(map);
boolean b = RSAUtils.verifySign(entries, publicKey); boolean verifyResult = RSAUtils.verifySign(entries, publicKey);
if (!b) { if (!verifyResult) {
log.error("yuzhou支付回调签名错误 {}", orderNo); log.error("yuzhou支付回调签名验证失败 orderNo: {}", orderNo);
log.error("yuzhou支付回调签名 {}", signature); log.error("yuzhou支付回调签名: {}", signatureObj);
log.error("yuzhou支付回调报文 {}", map); log.error("yuzhou支付回调完整报文: {}", map);
return null; return "FAIL";
} }
log.info("yuzhou支付回调签名验证成功 orderNo: {}", orderNo);
// 判断订单状态10表示成功
if (10 != status) {
log.warn("yuzhou支付订单状态非成功 orderNo: {}, status: {}", orderNo, status);
return "SUCCESS"; // 即使失败也返回SUCCESS避免重复回调
}
// 充值成功处理 // 充值成功处理
try {
aiRechargeService.addRecharge(orderNo); aiRechargeService.addRecharge(orderNo);
log.info("yuzhou支付回调处理成功 orderNo: {}", orderNo);
return "SUCCESS"; return "SUCCESS";
} catch (Exception e) {
log.error("yuzhou支付回调处理失败 orderNo: {}", orderNo, e);
return "FAIL";
}
} }

View File

@ -21,10 +21,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="source" column="source" /> <result property="source" column="source" />
<result property="text" column="text" /> <result property="text" column="text" />
<result property="isTop" column="is_top" /> <result property="isTop" column="is_top" />
<result property="img1" column="img1" />
<result property="img2" column="img2" />
<result property="mode" column="mode" />
<result property="duration" column="duration" />
<result property="resolution" column="resolution" />
<result property="ratio" column="ratio" />
<result property="model" column="model" />
<result property="videoParams" column="video_params" />
</resultMap> </resultMap>
<sql id="selectAiOrderVo"> <sql id="selectAiOrderVo">
select ao.id, ao.del_flag, ao.create_by, ao.create_time, ao.update_by, ao.update_time, ao.remark, ao.order_num, ao.user_id, ao.type, ao.amount, ao.result, ao.status, ao.source, ao.text, ao.is_top, au.user_id uuid from ai_order ao select ao.id, ao.del_flag, ao.create_by, ao.create_time, ao.update_by, ao.update_time, ao.remark, ao.order_num, ao.user_id, ao.type, ao.amount, ao.result, ao.status, ao.source, ao.text, ao.is_top, ao.img1, ao.img2, ao.mode, ao.duration, ao.resolution, ao.ratio, ao.model, ao.video_params, au.user_id uuid from ai_order ao
left join ai_user au on au.id = ao.user_id left join ai_user au on au.id = ao.user_id
</sql> </sql>
@ -61,6 +69,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where result = #{result} where result = #{result}
</select> </select>
<select id="getAiOrderByPortalVideoTask" resultMap="AiOrderResult">
<include refid="selectAiOrderVo"/>
where ao.del_flag = '0'
and ao.type = '21'
and (
ao.result = #{taskId}
or (
ao.video_params is not null and ao.video_params != ''
and JSON_VALID(ao.video_params)
and JSON_UNQUOTE(JSON_EXTRACT(ao.video_params, '$.volcTaskId')) = #{taskId}
)
)
limit 1
</select>
<select id="getSumAmountByUserId" resultType="java.math.BigDecimal"> <select id="getSumAmountByUserId" resultType="java.math.BigDecimal">
SELECT COALESCE(sum(amount), 0) from ai_order where user_id = #{userId} SELECT COALESCE(sum(amount), 0) from ai_order where user_id = #{userId}
</select> </select>
@ -118,7 +141,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="amount != null">amount = #{amount},</if> <if test="amount != null">amount = #{amount},</if>
<if test="result != null">result = #{result},</if> <if test="result != null">result = #{result},</if>
<if test="status != null">status = #{status},</if> <if test="status != null">status = #{status},</if>
<if test="source != null">status = #{source},</if> <if test="source != null">source = #{source},</if>
<if test="text != null">text = #{text},</if> <if test="text != null">text = #{text},</if>
<if test="isTop != null">is_top = #{isTop},</if> <if test="isTop != null">is_top = #{isTop},</if>
</trim> </trim>

View File

@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="chineseContent" column="chinese_content" /> <result property="chineseContent" column="chinese_content" />
<result property="englishContent" column="english_content" /> <result property="englishContent" column="english_content" />
<result property="imageUrl" column="image_url" /> <result property="imageUrl" column="image_url" />
<result property="aiId" column="ai_id" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
@ -20,7 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectAiTemplateVo"> <sql id="selectAiTemplateVo">
select id, name, chinese_content, english_content, image_url, status, remark, create_time, create_by, update_by, update_time, del_flag from ai_template select id, name, chinese_content, english_content, image_url, ai_id, status, remark, create_time, create_by, update_by, update_time, del_flag from ai_template
</sql> </sql>
<select id="selectAiTemplateList" parameterType="AiTemplate" resultMap="AiTemplateResult"> <select id="selectAiTemplateList" parameterType="AiTemplate" resultMap="AiTemplateResult">
@ -30,6 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="chineseContent != null and chineseContent != ''"> and chinese_content = #{chineseContent}</if> <if test="chineseContent != null and chineseContent != ''"> and chinese_content = #{chineseContent}</if>
<if test="englishContent != null and englishContent != ''"> and english_content = #{englishContent}</if> <if test="englishContent != null and englishContent != ''"> and english_content = #{englishContent}</if>
<if test="imageUrl != null and imageUrl != ''"> and image_url = #{imageUrl}</if> <if test="imageUrl != null and imageUrl != ''"> and image_url = #{imageUrl}</if>
<if test="aiId != null and aiId != ''"> and ai_id = #{aiId}</if>
<if test="status != null "> and status = #{status}</if> <if test="status != null "> and status = #{status}</if>
</where> </where>
</select> </select>
@ -46,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="chineseContent != null">chinese_content,</if> <if test="chineseContent != null">chinese_content,</if>
<if test="englishContent != null">english_content,</if> <if test="englishContent != null">english_content,</if>
<if test="imageUrl != null">image_url,</if> <if test="imageUrl != null">image_url,</if>
<if test="aiId != null">ai_id,</if>
<if test="status != null">status,</if> <if test="status != null">status,</if>
<if test="remark != null">remark,</if> <if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if> <if test="createTime != null">create_time,</if>
@ -59,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="chineseContent != null">#{chineseContent},</if> <if test="chineseContent != null">#{chineseContent},</if>
<if test="englishContent != null">#{englishContent},</if> <if test="englishContent != null">#{englishContent},</if>
<if test="imageUrl != null">#{imageUrl},</if> <if test="imageUrl != null">#{imageUrl},</if>
<if test="aiId != null">#{aiId},</if>
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
<if test="remark != null">#{remark},</if> <if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if> <if test="createTime != null">#{createTime},</if>
@ -76,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="chineseContent != null">chinese_content = #{chineseContent},</if> <if test="chineseContent != null">chinese_content = #{chineseContent},</if>
<if test="englishContent != null">english_content = #{englishContent},</if> <if test="englishContent != null">english_content = #{englishContent},</if>
<if test="imageUrl != null">image_url = #{imageUrl},</if> <if test="imageUrl != null">image_url = #{imageUrl},</if>
<if test="aiId != null">ai_id = #{aiId},</if>
<if test="status != null">status = #{status},</if> <if test="status != null">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if> <if test="remark != null">remark = #{remark},</if>
<if test="createTime != null">create_time = #{createTime},</if> <if test="createTime != null">create_time = #{createTime},</if>

View File

@ -33,15 +33,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="source" column="source" /> <result property="source" column="source" />
<result property="ip" column="ip" /> <result property="ip" column="ip" />
<result property="country" column="country" /> <result property="country" column="country" />
<result property="deptId" column="dept_id" />
<result property="deptName" column="dept_name" />
</resultMap> </resultMap>
<sql id="selectAiUserVo"> <sql id="selectAiUserVo">
select id, del_flag, create_by, create_time, update_by, update_time, remark, username, nickname, gender, avatar, phone, password, openid, status, email, birthday, invitation_code, payment_url, login_time, balance, superior_id, user_id, source, ip, country from ai_user select id, del_flag, create_by, create_time, update_by, update_time, remark, username, nickname, gender, avatar, phone, password, openid, status, email, birthday, invitation_code, payment_url, login_time, balance, superior_id, user_id, source, ip, country, dept_id from ai_user
</sql> </sql>
<select id="selectAiUserList" parameterType="AiUser" resultMap="AiUserResult"> <select id="selectAiUserList" parameterType="AiUser" resultMap="AiUserResult">
select u.id, u.del_flag, u.create_by, u.create_time, u.update_by, u.update_time, u.remark, u.username, u.nickname, u.gender, u.avatar, u.phone, u.password, u.openid, u.status, u.email, u.birthday, u.invitation_code, u.payment_url, u.login_time, u.balance, u.superior_id, u.user_id, u.source, u.ip, u.country, au.user_id superiorUuid, au.username superiorName from ai_user u select u.id, u.del_flag, u.create_by, u.create_time, u.update_by, u.update_time, u.remark, u.username, u.nickname, u.gender, u.avatar, u.phone, u.password, u.openid, u.status, u.email, u.birthday, u.invitation_code, u.payment_url, u.login_time, u.balance, u.superior_id, u.user_id, u.source, u.ip, u.country, u.dept_id, d.dept_name, au.user_id superiorUuid, au.username superiorName from ai_user u
left join ai_user au on au.id = u.superior_id left join ai_user au on au.id = u.superior_id
left join sys_dept d on d.dept_id = u.dept_id and d.del_flag = '0'
<where> <where>
<if test="nickname != null and nickname != ''"> and u.nickname like concat('%', #{nickname}, '%')</if> <if test="nickname != null and nickname != ''"> and u.nickname like concat('%', #{nickname}, '%')</if>
<if test="username != null and username != ''"> and u.username like concat('%', #{username}, '%')</if> <if test="username != null and username != ''"> and u.username like concat('%', #{username}, '%')</if>
@ -63,19 +66,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="superiorId != null "> and u.superior_id = #{superiorId}</if> <if test="superiorId != null "> and u.superior_id = #{superiorId}</if>
<if test="id != null "> and u.id = #{id}</if> <if test="id != null "> and u.id = #{id}</if>
<if test="source != null "> and u.source = #{source}</if> <if test="source != null "> and u.source = #{source}</if>
<if test="deptId != null "> and u.dept_id = #{deptId}</if>
</where> </where>
order by u.id desc order by u.id desc
</select> </select>
<select id="selectAiUserById" parameterType="Long" resultMap="AiUserResult"> <select id="selectAiUserById" parameterType="Long" resultMap="AiUserResult">
<include refid="selectAiUserVo"/> select u.id, u.del_flag, u.create_by, u.create_time, u.update_by, u.update_time, u.remark, u.username, u.nickname, u.gender, u.avatar, u.phone, u.password, u.openid, u.status, u.email, u.birthday, u.invitation_code, u.payment_url, u.login_time, u.balance, u.superior_id, u.user_id, u.source, u.ip, u.country, u.dept_id, d.dept_name
where id = #{id} from ai_user u
left join sys_dept d on d.dept_id = u.dept_id and d.del_flag = '0'
where u.id = #{id}
</select> </select>
<select id="getUserInfo" parameterType="Long" resultMap="AiUserResult"> <select id="getUserInfo" parameterType="Long" resultMap="AiUserResult">
select del_flag, create_by, create_time, update_by, update_time, remark, username, nickname, gender, avatar, phone, password, openid, status, email, birthday, invitation_code, payment_url, login_time, balance, superior_id, user_id, source from ai_user select del_flag, create_by, create_time, update_by, update_time, remark, username, nickname, gender, avatar, phone, password, openid, status, email, birthday, invitation_code, payment_url, login_time, balance, superior_id, user_id, source, ip, country, dept_id from ai_user
where id = #{id} where id = #{id}
</select> </select>
<select id="countAiUserByDeptId" resultType="int">
select count(1) from ai_user where del_flag = '0' and dept_id = #{deptId}
</select>
<select id="selectPasswordById" resultType="java.lang.String"> <select id="selectPasswordById" resultType="java.lang.String">
select password from ai_user where id = #{id} select password from ai_user where id = #{id}
</select> </select>
@ -108,6 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="source != null">source,</if> <if test="source != null">source,</if>
<if test="ip != null">ip,</if> <if test="ip != null">ip,</if>
<if test="country != null">country,</if> <if test="country != null">country,</if>
<if test="deptId != null">dept_id,</if>
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="delFlag != null and delFlag != ''">#{delFlag},</if> <if test="delFlag != null and delFlag != ''">#{delFlag},</if>
@ -134,6 +145,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="userId != null">#{userId},</if> <if test="userId != null">#{userId},</if>
<if test="source != null">#{source},</if> <if test="source != null">#{source},</if>
<if test="country != null">#{country},</if> <if test="country != null">#{country},</if>
<if test="deptId != null">#{deptId},</if>
</trim> </trim>
</insert> </insert>
@ -163,8 +175,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="superiorId != null">superior_id = #{superiorId},</if> <if test="superiorId != null">superior_id = #{superiorId},</if>
<if test="userId != null">user_id = #{userId},</if> <if test="userId != null">user_id = #{userId},</if>
<if test="source != null">source = #{source},</if> <if test="source != null">source = #{source},</if>
<if test="ip != null">source = #{ip},</if> <if test="ip != null">ip = #{ip},</if>
<if test="country != null">country = #{country},</if> <if test="country != null">country = #{country},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
</trim> </trim>
where id = #{id} where id = #{id}
</update> </update>

View File

@ -13,6 +13,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="leader" column="leader" /> <result property="leader" column="leader" />
<result property="phone" column="phone" /> <result property="phone" column="phone" />
<result property="email" column="email" /> <result property="email" column="email" />
<result property="byteApiKey" column="byte_api_key" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="delFlag" column="del_flag" /> <result property="delFlag" column="del_flag" />
<result property="parentName" column="parent_name" /> <result property="parentName" column="parent_name" />
@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectDeptVo"> <sql id="selectDeptVo">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.status, d.del_flag, d.create_by, d.create_time
from sys_dept d from sys_dept d
</sql> </sql>
@ -59,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult"> <select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.status,
(select dept_name from sys_dept where dept_id = d.parent_id) parent_name (select dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d from sys_dept d
where d.dept_id = #{deptId} where d.dept_id = #{deptId}
@ -101,6 +102,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null and leader != ''">leader,</if> <if test="leader != null and leader != ''">leader,</if>
<if test="phone != null and phone != ''">phone,</if> <if test="phone != null and phone != ''">phone,</if>
<if test="email != null and email != ''">email,</if> <if test="email != null and email != ''">email,</if>
<if test="byteApiKey != null">byte_api_key,</if>
<if test="status != null">status,</if> <if test="status != null">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
create_time create_time
@ -113,6 +115,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null and leader != ''">#{leader},</if> <if test="leader != null and leader != ''">#{leader},</if>
<if test="phone != null and phone != ''">#{phone},</if> <if test="phone != null and phone != ''">#{phone},</if>
<if test="email != null and email != ''">#{email},</if> <if test="email != null and email != ''">#{email},</if>
<if test="byteApiKey != null">#{byteApiKey},</if>
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate() sysdate()
@ -129,6 +132,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null">leader = #{leader},</if> <if test="leader != null">leader = #{leader},</if>
<if test="phone != null">phone = #{phone},</if> <if test="phone != null">phone = #{phone},</if>
<if test="email != null">email = #{email},</if> <if test="email != null">email = #{email},</if>
<if test="byteApiKey != null">byte_api_key = #{byteApiKey},</if>
<if test="status != null and status != ''">status = #{status},</if> <if test="status != null and status != ''">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
update_time = sysdate() update_time = sysdate()