diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/AiUserApiController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/AiUserApiController.java index 3d6cc81..7bdcea8 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/AiUserApiController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/AiUserApiController.java @@ -2,12 +2,9 @@ package com.ruoyi.api; import com.ruoyi.ai.domain.AiSampleAmount; 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.service.EmailVerifyService; import com.ruoyi.ai.service.IAiSampleAmountRecordService; -import com.ruoyi.ai.service.IAiStatisticsService; import com.ruoyi.ai.service.IAiUserService; import com.ruoyi.common.annotation.Anonymous; 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.job.TaskException; import com.ruoyi.common.utils.DateUtils; -import com.ruoyi.common.utils.IpCountryQueryByApi; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.SecurityUtils; -import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.framework.web.service.SysLoginService; import com.ruoyi.quartz.domain.SysJob; import com.ruoyi.quartz.service.ISysJobService; @@ -40,7 +35,10 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.mail.MessagingException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Map; +import java.util.UUID; /** @@ -104,8 +102,11 @@ public class AiUserApiController extends BaseController { // 查询启用状态体验金活动 AiSampleAmount aiSampleAmount = aiSampleAmountMapper.getSampleAmount(); 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.setUserId(aiUser.getId()); @@ -218,6 +219,7 @@ public class AiUserApiController extends BaseController { /** * 查询返佣比例配置 + * * @return */ @GetMapping("/getRebateConfig") diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/CosController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/CosController.java new file mode 100644 index 0000000..d188e10 --- /dev/null +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/CosController.java @@ -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; + } +} diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/FileController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/FileController.java index fefd1e0..2dcc9bb 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/FileController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/FileController.java @@ -1,7 +1,7 @@ package com.ruoyi.api; 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.ApiOperation; import io.swagger.annotations.ApiParam; @@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile; @Api(tags = "文件上传") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class FileController { - private final AwsS3Util awsS3Util; + private final TencentCosUtil tencentCosUtil; /** * 文件上传 @@ -30,7 +30,7 @@ public class FileController { @ApiParam(name = "file", value = "文件", required = true) @RequestParam("file") MultipartFile file) throws Exception { AjaxResult ajax = AjaxResult.success(); - String uploadUrl = awsS3Util.uploadMultipartFile(file, true); + String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true); ajax.put("url", uploadUrl); return ajax; } diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/KadaPaymentJava.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/KadaPaymentJava.java index 93d2ae2..93dafb0 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/KadaPaymentJava.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/KadaPaymentJava.java @@ -69,8 +69,8 @@ public class KadaPaymentJava { map.put("pm", "QRPH"); map.put("ref", orderNo); map.put("payer", payer); - map.put("redirect", "www.google.com/recharge"); - map.put("callbackUrl", "www.google.com/api/pay/kada-callBack"); + map.put("redirect", "https://undressing.top/recharge"); + map.put("callbackUrl", "https://undressing.top/api/pay/kada-callBack"); return JSON.toJSONString(map); } diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PayController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PayController.java index 871c77a..cae904e 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PayController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PayController.java @@ -5,8 +5,10 @@ import com.ruoyi.ai.service.IAiRechargeService; import com.ruoyi.ai.service.IJinShaService; import com.ruoyi.ai.service.IKaDaService; import com.ruoyi.ai.service.IYuZhouService; +import com.ruoyi.ai.service.IVmService; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ip.IpUtils; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -25,6 +27,7 @@ public class PayController { private final IKaDaService kaDaService; private final IAiRechargeService aiRechargeService; private final IYuZhouService yuZhouService; + private final IVmService vmService; /** * jinsha请求支付 @@ -85,4 +88,109 @@ public class PayController { 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 paramNames = request.getParameterNames(); + java.util.Map 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")); + + // 解析时间戳:createdAt、successTime、reqTime(文档字段名) + 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); + } + } diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/ByteApiRequest.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/ByteApiRequest.java index 9c04dab..f0e5e61 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/ByteApiRequest.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/ByteApiRequest.java @@ -31,6 +31,11 @@ public class ByteApiRequest { @ApiModelProperty(name = "标签字符串") private String tags; + @ApiModelProperty(name = "使用的模型") + private String model; + + @ApiModelProperty(name = "生成模式:text-to-video 或 image-to-video") + private String mode = "text-to-video"; } diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java new file mode 100644 index 0000000..eea6099 --- /dev/null +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java @@ -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 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; +} diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiDeptController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiDeptController.java new file mode 100644 index 0000000..8593912 --- /dev/null +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiDeptController.java @@ -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 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 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)); + } +} diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiTemplateController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiTemplateController.java index c05daf8..15901b1 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiTemplateController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiTemplateController.java @@ -1,4 +1,4 @@ -package com.ruoyi.ai.controller; +package com.ruoyi.web.controller.ai; import java.util.List; import javax.servlet.http.HttpServletResponse; diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiUserController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiUserController.java index d0785f0..8e1d350 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiUserController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ai/AiUserController.java @@ -1,33 +1,27 @@ package com.ruoyi.web.controller.ai; import cn.hutool.core.util.NumberUtil; -import com.ruoyi.ai.service.EmailVerifyService; import com.ruoyi.ai.service.IAiUserService; -import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.constant.BalanceChangerConstants; -import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; 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.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 io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; 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.*; -import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import java.util.UUID; /** * ai-用户信息Controller @@ -77,7 +71,6 @@ public class AiUserController extends BaseController { } - /** * 新增ai-用户信息 */ @@ -100,6 +93,20 @@ public class AiUserController extends BaseController { 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 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()); - aiUserService.addUserBalance(aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION); + aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION); return toAjax(1); } @@ -151,8 +161,6 @@ public class AiUserController extends BaseController { } - - // @GetMapping("/addUserBalance") // public AjaxResult addUserBalance(BigDecimal amount) { // aiUserService.addUserBalance(SecurityUtils.getAiUserId(), amount, BalanceChangerConstants.RECHARGE); diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java index 843c9ba..0f2ff29 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -4,7 +4,6 @@ import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.exception.base.BaseException; -import com.ruoyi.common.utils.AwsS3Util; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.TencentCosUtil; import com.ruoyi.common.utils.file.FileUploadUtils; @@ -36,7 +35,6 @@ public class CommonController { private static final Logger log = LoggerFactory.getLogger(CommonController.class); private final ServerConfig serverConfig; private final TencentCosUtil tencentCosUtil; - private final AwsS3Util awsS3Util; /** * 通用下载请求 @@ -158,7 +156,7 @@ public class CommonController { } /** - * AWS上传请求(单个) + * 腾讯云COS上传请求(单个) */ @ApiOperation("图片上传接口") @PostMapping("/aws/upload") @@ -166,7 +164,7 @@ public class CommonController { AjaxResult ajax = AjaxResult.success(); String uploadUrl; try { - uploadUrl = awsS3Util.uploadMultipartFile(file, true); + uploadUrl = tencentCosUtil.uploadMultipartFile(file, true); } catch (Exception e) { return AjaxResult.error(e.getMessage()); } diff --git a/web-api/ruoyi-admin/src/main/resources/application-druid.yml b/web-api/ruoyi-admin/src/main/resources/application-druid.yml index 52bbeab..add300e 100644 --- a/web-api/ruoyi-admin/src/main/resources/application-druid.yml +++ b/web-api/ruoyi-admin/src/main/resources/application-druid.yml @@ -5,9 +5,9 @@ spring: druid: # 主库数据源 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 - password: xxxxxx + password: mkMReisAKl6I7rVqEY90 driverClassName: com.mysql.cj.jdbc.Driver # 初始连接数 @@ -41,8 +41,8 @@ spring: allow: url-pattern: /druid/* # 控制台管理用户名和密码 - login-username: - login-password: + login-username: ruoyi + login-password: cvs822qoc391 filter: stat: enabled: true @@ -54,3 +54,4 @@ spring: config: multi-statement-allow: true + diff --git a/web-api/ruoyi-common/pom.xml b/web-api/ruoyi-common/pom.xml index b369ff8..1268461 100644 --- a/web-api/ruoyi-common/pom.xml +++ b/web-api/ruoyi-common/pom.xml @@ -71,6 +71,12 @@ commons-io + + + commons-codec + commons-codec + + org.apache.poi diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AiUser.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AiUser.java index 559d070..0cd4948 100644 --- a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AiUser.java +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AiUser.java @@ -117,12 +117,19 @@ public class AiUser extends BaseEntity { */ private String country; + /** 归属部门ID(sys_dept.dept_id) */ + private Long deptId; + /** * 上级用户昵称 */ @TableField(exist = false) private String superiorName; + /** 归属部门名称 */ + @TableField(exist = false) + private String deptName; + /** * 上级用户ID */ diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java index fb18c5c..f12e4d8 100644 --- a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java @@ -51,6 +51,9 @@ public class SysDept extends BaseEntity /** 父部门名称 */ private String parentName; + + /** Byte API Key */ + private String byteApiKey; /** 子部门 */ private List children = new ArrayList(); @@ -171,6 +174,16 @@ public class SysDept extends BaseEntity this.parentName = parentName; } + public String getByteApiKey() + { + return byteApiKey; + } + + public void setByteApiKey(String byteApiKey) + { + this.byteApiKey = byteApiKey; + } + public List getChildren() { return children; @@ -192,6 +205,7 @@ public class SysDept extends BaseEntity .append("leader", getLeader()) .append("phone", getPhone()) .append("email", getEmail()) + .append("byteApiKey", getByteApiKey()) .append("status", getStatus()) .append("delFlag", getDelFlag()) .append("createBy", getCreateBy()) diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/TencentCosUtil.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/TencentCosUtil.java index dde4e7d..8dfad2e 100644 --- a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/TencentCosUtil.java +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/TencentCosUtil.java @@ -13,7 +13,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; 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; @Component @@ -33,22 +39,32 @@ public class TencentCosUtil { 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(); - // 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式 - // 对象键(Key)是对象在存储桶中的唯一标识。 998u-09iu-09i-333 - //在文件名称前面添加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; + // 生成唯一文件键,格式与AWS一致:yyyy/MM/dd/uuid_filename + String key = generateCosKey(file.getOriginalFilename()); + try { - //获取上传文件输入流 InputStream inputStream = file.getInputStream(); ObjectMetadata objectMetadata = new ObjectMetadata(); PutObjectRequest putObjectRequest = new PutObjectRequest( @@ -56,17 +72,41 @@ public class TencentCosUtil { key, inputStream, objectMetadata); - // 高级接口会返回一个异步结果Upload + PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest); - //返回上传文件路径 - //https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg - String url = domain + "/" + key; + // 返回COS文件访问地址 + String url = domain + (domain.endsWith("/") ? "" : "/") + key; return url; } catch (Exception e) { 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); // 这里建议设置使用 https 协议 clientConfig.setHttpProtocol(HttpProtocol.https); + clientConfig.setConnectionTimeout(30 * 1000); // 连接超时30秒 + clientConfig.setSocketTimeout(60 * 1000); // 读取超时60秒 //1.3 生成cos客户端 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()); + } + }; + } } diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/RSAUtils.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/RSAUtils.java index 8229dd7..02170be 100644 --- a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/RSAUtils.java +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/RSAUtils.java @@ -1,14 +1,19 @@ package com.ruoyi.common.utils.sign; 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.lang3.ArrayUtils; import javax.crypto.Cipher; import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; @@ -16,25 +21,29 @@ import java.util.Collections; import java.util.List; import java.util.Map; +@Slf4j public class RSAUtils { - // RSA最⼤加密明⽂⼤⼩ + // RSA最大加密明文大小 private static final int MAX_ENCRYPT_BLOCK = 117; - // 不仅可以使⽤DSA算法,同样也可以使⽤RSA算法做数字签名 + // 不仅可以使用DSA算法,同样也可以使用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编码的公钥 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加密 Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, priKey); byte[] data = str.getBytes("UTF-8"); - // 加密时超过117字节就报错。为此采⽤分段加密的办法来加密 + // 加密时超过117字节就报错。为此采用分段加密的办法来加密 byte[] enBytes = null; 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)); enBytes = ArrayUtils.addAll(enBytes, doFinal); } @@ -43,15 +52,22 @@ public class RSAUtils { } private static String encryptBASE64(byte[] data) { - return new String(Base64.encode(data)); + return new String(Base64.encodeBase64(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) { String platSign = params.getStr("signature"); // sign + log.info("signature:" + platSign); List paramNameList = new ArrayList<>(); for (String key : params.keySet()) { if (!"signature".equals(key)) { @@ -64,13 +80,14 @@ public class RSAUtils { String name = paramNameList.get(i); stringBuilder.append(params.getStr(name)); } + log.info("keys:" + stringBuilder); String decryptSign = ""; try { - decryptSign = publicDecrypt(platSign, getPublicKey(publickey) - ); + decryptSign = publicDecrypt(platSign, getPublicKey(publickey)); } catch (Exception e) { - System.out.println(e.toString()); + log.error("Error decrypting signature", e); } + log.info("decryptSign:" + decryptSign); if (!stringBuilder.toString().equalsIgnoreCase(decryptSign)) { return false; } @@ -89,11 +106,12 @@ public class RSAUtils { stringBuilder.append(createMap.get(key)); // 拼接参数 } String keyStr = stringBuilder.toString(); // 得到待加密的字符串 + log.info("keyStr:" + keyStr); String signedStr = ""; try { signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密 } catch (Exception e) { - System.out.println(e.toString()); + log.error("Error encrypting signature", e); } return signedStr; } @@ -110,47 +128,70 @@ public class RSAUtils { stringBuilder.append(createMap.get(key)); // 拼接参数 } String keyStr = stringBuilder.toString(); // 得到待加密的字符串 + log.info("keyStr:" + keyStr); String signedStr = ""; try { signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密 } catch (Exception e) { - System.out.println(e.toString()); + log.error("Error encrypting signature", e); } return signedStr; } + /** + * private key encryption + * + * @param data + * @param privateKey + * @return + */ public static String privateEncrypt(String data, RSAPrivateKey privateKey) { try { Cipher cipher = Cipher.getInstance("RSA"); 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) { - throw new RuntimeException("Exception encountered while encry pting string [" + data + "]", e); + throw new RuntimeException("Exception encountered while encrypting string [" + data + "]", e); } } + /** + * public key decryption + */ public static String publicDecrypt(String data, RSAPublicKey publicKey) { try { Cipher cipher = Cipher.getInstance("RSA"); 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) { - throw new RuntimeException("An exception was encountered whil e 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 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); 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 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); return key; } @@ -179,7 +220,7 @@ public class RSAUtils { offSet = i * maxBlock; } } catch (Exception e) { - throw new RuntimeException("An exception occurred when encryp ting and decrypting data whose threshold is [" + maxBlock + "]", e); + throw new RuntimeException("An exception occurred when encrypting and decrypting data whose threshold is [" + maxBlock + "]", e); } byte[] resultDatas = out.toByteArray(); IOUtils.closeQuietly(out); diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiBalanceChangeRecord.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiBalanceChangeRecord.java index 1eaa495..989672f 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiBalanceChangeRecord.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiBalanceChangeRecord.java @@ -1,16 +1,14 @@ package com.ruoyi.ai.domain; -import java.math.BigDecimal; - 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 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 lombok.Data; + +import java.math.BigDecimal; /** * 余额使用记录对象 ai_balance_change_record @@ -24,13 +22,23 @@ public class AiBalanceChangeRecord extends BaseEntity { private static final long serialVersionUID = 1L; - /** 主键ID */ + /** + * 主键ID + */ @TableId(type = IdType.AUTO) private Long id; - /** 删除标志(0代表存在 2代表删除) */ + /** + * 删除标志(0代表存在 2代表删除) + */ private String delFlag; + /** + * 关联订单号 + */ + @Excel(name = "关联订单号") + private String orderNo; + /** * 用户ID */ @@ -42,19 +50,27 @@ public class AiBalanceChangeRecord extends BaseEntity { @TableField(exist = false) 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 = "操作类型") private Integer type; - /** 变更金额 */ + /** + * 变更金额 + */ @Excel(name = "变更金额") private BigDecimal changeAmount; - /** 变更后金额 */ + /** + * 变更后金额 + */ @Excel(name = "变更后金额") private BigDecimal resultAmount; - /** 用户昵称 */ + /** + * 用户昵称 + */ @TableField(exist = false) @Excel(name = "用户昵称") private String nickname; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java index 1b33015..3938055 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java @@ -68,6 +68,28 @@ public class AiOrder extends BaseEntity { @Excel(name = "是否置顶:N-否 Y-是") 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; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiRebateRecord.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiRebateRecord.java index 4b2f905..d729041 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiRebateRecord.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiRebateRecord.java @@ -1,16 +1,15 @@ 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 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 * @@ -23,22 +22,38 @@ public class AiRebateRecord extends BaseEntity { private static final long serialVersionUID = 1L; - /** 主键ID */ + /** + * 主键ID + */ @TableId(type = IdType.AUTO) private Long id; - /** 删除标志(0代表存在 2代表删除) */ + /** + * 关联订单号 + */ + @Excel(name = "关联订单号") + private String orderNo; + + /** + * 删除标志(0代表存在 2代表删除) + */ private String delFlag; - /** 上级ID */ + /** + * 上级ID + */ @Excel(name = "上级ID") private Long superiorId; - /** 下级ID */ + /** + * 下级ID + */ @Excel(name = "下级ID") private Long subordinateId; - /** 返佣金额 */ + /** + * 返佣金额 + */ @Excel(name = "返佣金额") private BigDecimal amount; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiSampleAmountRecord.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiSampleAmountRecord.java index 7fb0f99..64a8541 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiSampleAmountRecord.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiSampleAmountRecord.java @@ -1,18 +1,17 @@ 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.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 * @@ -25,22 +24,38 @@ public class AiSampleAmountRecord extends BaseEntity { private static final long serialVersionUID = 1L; - /** 主键ID */ + /** + * 主键ID + */ @TableId(type = IdType.AUTO) private Long id; - /** 删除标志(0代表存在 2代表删除) */ + /** + * 关联订单号 + */ + @Excel(name = "关联订单号") + private String orderNo; + + /** + * 删除标志(0代表存在 2代表删除) + */ private String delFlag; - /** 用户ID */ + /** + * 用户ID + */ @Excel(name = "用户ID") private Long userId; - /** 体验金额 */ + /** + * 体验金额 + */ @Excel(name = "体验金额") private BigDecimal sampleAmount; - /** 回收时间 */ + /** + * 回收时间 + */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "回收时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date recycleTime; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiTemplate.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiTemplate.java index 08611e0..6b74db8 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiTemplate.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiTemplate.java @@ -43,6 +43,10 @@ public class AiTemplate extends BaseEntity { @Excel(name = "模版图片URL") private String imageUrl; + /** AI类型 */ + @Excel(name = "AI类型") + private Long aiId; + /** 状态:0-禁用,1-启用 */ @Excel(name = "状态:0-禁用,1-启用") private Integer status; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ByteBodyReq.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ByteBodyReq.java index 63d46fa..1d1a320 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ByteBodyReq.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ByteBodyReq.java @@ -30,5 +30,10 @@ public class ByteBodyReq { @JsonProperty("content") private List content; + private Integer duration; + private String resolution; + private String ratio; + private Integer seed; + } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ContentItem.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ContentItem.java index e0630d5..97ce2c6 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ContentItem.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/ContentItem.java @@ -15,6 +15,12 @@ public class ContentItem { @JsonProperty("image_url") private ImageUrl imageUrl; + @JsonProperty("video_url") + private ImageUrl videoUrl; + + @JsonProperty("audio_url") + private ImageUrl audioUrl; + @JsonProperty("role") private String role; } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/JinShaBodyReq.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/JinShaBodyReq.java index 80c7a37..cb68c21 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/JinShaBodyReq.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/JinShaBodyReq.java @@ -21,9 +21,9 @@ public class JinShaBodyReq { private String notify_url; - private String return_url; - private String sign; + private String cardname; + private String cardno; /** * 档位ID diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCallBackReq.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCallBackReq.java new file mode 100644 index 0000000..13b1628 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCallBackReq.java @@ -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; +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCardInfo.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCardInfo.java new file mode 100644 index 0000000..82eb751 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmCardInfo.java @@ -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; +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmPayReq.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmPayReq.java new file mode 100644 index 0000000..98b482e --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmPayReq.java @@ -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; +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderReq.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderReq.java new file mode 100644 index 0000000..577df4c --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderReq.java @@ -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; +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderRes.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderRes.java new file mode 100644 index 0000000..f77b9f5 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/VmUnifiedOrderRes.java @@ -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; + } +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java index 44e98be..cafa00d 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java @@ -22,5 +22,8 @@ public interface AiOrderMapper extends BaseMapper { AiOrder getAiOrderByResult(@Param("result") String result); + /** 门户视频:按火山任务 id 查单(result 可能已被替换为成品 URL,故兼查 video_params.volcTaskId) */ + AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId); + BigDecimal getSumAmountByUserId(@Param("userId") String userId); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiUserMapper.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiUserMapper.java index 9cd1763..d5c8c74 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiUserMapper.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiUserMapper.java @@ -6,6 +6,7 @@ import java.util.List; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.common.core.domain.entity.AiUser; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; /** * ai-用户信息Mapper接口 @@ -22,4 +23,9 @@ public interface AiUserMapper extends BaseMapper { AiUser selectAiUserById(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); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java index 641c287..1cbe6cd 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java @@ -79,5 +79,7 @@ public interface IAiOrderService { AiOrder getAiOrderByResult(String result); + AiOrder getAiOrderByPortalVideoTask(String taskId); + BigDecimal getSumAmountByUserId(String userId); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiUserService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiUserService.java index 7ff4540..6ce243e 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiUserService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiUserService.java @@ -87,9 +87,9 @@ public interface IAiUserService { 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); @@ -99,9 +99,19 @@ public interface IAiUserService { int updatePassword(AiUser aiUser); + /** + * 部门下 AI 用户数量(用于删除部门前校验) + */ + int countAiUserByDeptId(Long deptId); + + /** + * 分配 AI 用户归属部门,deptId 为空则清空 + */ + int updateAiUserDept(Long userId, Long deptId); + public void deductSampleAmount(String userId); - void handleRebate(Long userId, BigDecimal rechargeAmount); + void handleRebate(String orderNo, Long userId, BigDecimal rechargeAmount); String getRebateConfig(); diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteDeptApiKeyService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteDeptApiKeyService.java new file mode 100644 index 0000000..bf2a8ef --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteDeptApiKeyService.java @@ -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); +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java index 137fd63..ef42e36 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java @@ -2,6 +2,7 @@ package com.ruoyi.ai.service; import com.ruoyi.ai.domain.ByteBodyReq; import com.ruoyi.ai.domain.ByteBodyRes; +import com.ruoyi.common.core.domain.AjaxResult; public interface IByteService { @@ -16,12 +17,31 @@ public interface IByteService { ByteBodyRes imgToImg(ByteBodyReq req) throws Exception; /** - * 首尾帧图生视频 + * 首尾帧图生视频(使用全局配置的 Ark API Key) */ 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, 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; } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java new file mode 100644 index 0000000..5f57e98 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java @@ -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; +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IJinShaService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IJinShaService.java index 1fdd219..e995765 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IJinShaService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IJinShaService.java @@ -8,6 +8,7 @@ public interface IJinShaService { /** * 支付 + * @param gearId 档位ID */ PayResVO jinShaPay(Long gearId) throws Exception; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IVmService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IVmService.java new file mode 100644 index 0000000..baca39e --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IVmService.java @@ -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); +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiRechargeServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiRechargeServiceImpl.java index 19dd73c..e5b3475 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiRechargeServiceImpl.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiRechargeServiceImpl.java @@ -1,29 +1,24 @@ 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.util.Date; 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业务层处理 * @@ -84,6 +79,10 @@ public class AiRechargeServiceImpl implements IAiRechargeService { public void addRecharge(String orderNo) { // 设置到账时间 AiRecharge aiRecharge = this.getAiRechargeByOrderNum(orderNo); + if (aiRecharge == null) { + log.error("支付回调订单不存在,orderNo: {}", orderNo); + return; + } Date creditedTime = aiRecharge.getCreditedTime(); if (creditedTime != null) { log.error("支付回调重复 {}", orderNo); @@ -107,12 +106,10 @@ public class AiRechargeServiceImpl implements IAiRechargeService { 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.setSource(aiRecharge.getSource()); @@ -120,14 +117,14 @@ public class AiRechargeServiceImpl implements IAiRechargeService { statisticsService.saveOrUpdateData(aiStatistics); - // 处理赠送金额 + // 处理赠送金额 if (isFristRecharge && aiRecharge.getGearId() != null) { // 查询活动是否过期 boolean isAvailable = aiRechargeGiftService.isActivityAvailable(aiRechargeGiftGear.getRechargeId()); if (isAvailable) { 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.setOrderNum(orderNo); diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiUserServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiUserServiceImpl.java index e101b88..69c2bc4 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiUserServiceImpl.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiUserServiceImpl.java @@ -9,20 +9,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ai.domain.*; import com.ruoyi.ai.domain.enums.AiConfigEnum; import com.ruoyi.ai.mapper.AiRechargeMapper; -import com.ruoyi.ai.mapper.AiSampleAmountMapper; -import com.ruoyi.ai.mapper.AiSampleAmountRecordMapper; +import com.ruoyi.ai.mapper.AiUserMapper; import com.ruoyi.ai.service.*; import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.constant.HttpStatus; 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.LoginAiUserBody; import com.ruoyi.common.core.domain.model.RegisterAiUserBody; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.*; import com.ruoyi.common.utils.ip.IpUtils; -import com.ruoyi.common.utils.uuid.UUID; import com.ruoyi.system.domain.SysConfig; import com.ruoyi.system.mapper.SysConfigMapper; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +27,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -70,7 +66,6 @@ public class AiUserServiceImpl implements IAiUserService { private AiRechargeMapper aiRechargeMapper; - /** * 查询ai-用户信息 * @@ -137,6 +132,19 @@ public class AiUserServiceImpl implements IAiUserService { 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-用户信息 * @@ -208,7 +216,7 @@ public class AiUserServiceImpl implements IAiUserService { // 新增访客统计 AiStatistics aiStatistics = new AiStatistics(); aiStatistics.setSource(registerAiUserBody.getSource()); - aiStatistics.setAddUserCount(1l); + aiStatistics.setAddUserCount(1L); aiStatisticsService.saveOrUpdateData(aiStatistics); return aiUser; } @@ -352,13 +360,13 @@ public class AiUserServiceImpl implements IAiUserService { } @Override - public void addUserBalance(Long userId, BigDecimal amount, int recharge) { - addUserBalance(userId, amount, recharge, null); + public void addUserBalance(String orderNo, Long userId, BigDecimal amount, int recharge) { + addUserBalance(orderNo, userId, amount, recharge, null); } @Override @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); AiBalanceChangeRecord balanceChangeRecord = new AiBalanceChangeRecord(); @@ -367,6 +375,7 @@ public class AiUserServiceImpl implements IAiUserService { balanceChangeRecord.setChangeAmount(amount); balanceChangeRecord.setResultAmount(NumberUtil.add(amount, aiUser.getBalance())); balanceChangeRecord.setRemark(remark); + balanceChangeRecord.setOrderNo(orderNo); balanceChangeRecordService.insertAiBalanceChangeRecord(balanceChangeRecord); // 修改用户余额 aiUser.setBalance(balanceChangeRecord.getResultAmount()); @@ -396,7 +405,7 @@ public class AiUserServiceImpl implements IAiUserService { public int updatePassword(AiUser aiUser) { aiUser.setPassword(SecurityUtils.encryptPassword(aiUser.getNewPassword())); return aiUserMapper.updateById(aiUser); - } + } @Override @Transactional @@ -410,14 +419,14 @@ public class AiUserServiceImpl implements IAiUserService { // if (deductAmount.compareTo(BigDecimal.ZERO) < 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); aiSampleAmountRecordService.updateAiSampleAmountRecord(aiSampleAmountRecord); } @Override - public void handleRebate(Long userId, BigDecimal amount) { + public void handleRebate(String orderNo, Long userId, BigDecimal amount) { AiUser aiUser = aiUserMapper.selectById(userId); if (aiUser.getSuperiorId() == null) return; // 查询上级用户 @@ -428,7 +437,7 @@ public class AiUserServiceImpl implements IAiUserService { SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique(AiConfigEnum.SYS_INVITE_COMMISSION.getValue()); double decimal = NumberUtil.div(Integer.parseInt(sysConfig.getConfigValue()), 100, 2); BigDecimal rebateAmount = NumberUtil.mul(amount, decimal); - addUserBalance(superiorUser.getId(), rebateAmount, BalanceChangerConstants.REBATE); + addUserBalance(orderNo, superiorUser.getId(), rebateAmount, BalanceChangerConstants.REBATE); // 新增返佣记录 AiRebateRecord rebateRecord = new AiRebateRecord(); rebateRecord.setSuperiorId(superiorUser.getId()); diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java new file mode 100644 index 0000000..5c03a99 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java @@ -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(); + } +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java new file mode 100644 index 0000000..4a88746 --- /dev/null +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java @@ -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 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 rates; + } +} diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java index 45cbcc0..0612fac 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java @@ -47,6 +47,10 @@ public class JinShaService implements IJinShaService { private IAiUserService aiUserService; @Autowired private IAiRechargeGiftGearService aiRechargeGiftGearService; + @Autowired + private IAiRechargeGiftService aiRechargeGiftService; + @Autowired + private IExchangeRateService exchangeRateService; @Override 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); } 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 params = new TreeMap<>(); - params.put("appid", req.getAppid()); + // 生成订单号 String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); String orderNo = dateTime + "js" + uuid; - params.put("orderno", orderNo); - params.put("amount", amount); - params.put("notify_url", req.getNotify_url()); - params.put("return_url", req.getReturn_url()); - StringBuilder append = this.append(params); - String sign = this.sign(append); - String formData = append + "&sign=" + sign; - // 2. 构建请求体(form-urlencoded 格式) - RequestBody requestBody = RequestBody.create( - MediaType.get("application/x-www-form-urlencoded;charset=utf-8"), - formData - ); - // 3. 构建 POST 请求 + + // 金额转换为number类型(保留两位小数) + String amountStr = String.format("%.2f", amount.doubleValue()); + + // 构建回调地址和返回地址 + String notifyUrlFull = notifyUrl + "/api/pay/jinsha-callBack"; + String returnUrlFull = returnUrl + "/recharge"; + + // 1. 配置签名参数(按照key排序:appid, amount, orderno, notify_url, return_url,不包含sign) + TreeMap signParams = new TreeMap<>(); + signParams.put("appid", appId); + signParams.put("amount", amountStr); + 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() .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) .build(); - // 4. 发送请求并获取响应 + + // 5. 发送请求并获取响应 Response response = OkHttpUtils.newCall(request).execute(); ResponseBody body = response.body(); if (null == body) { - log.error("kada支付请求的响应异常"); - throw new Exception("kadapay responsebody is null"); + log.error("jinsha支付请求的响应异常"); + 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); + + // 检查响应code,0表示成功,其他值表示失败 + 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()); AiRecharge aiRecharge = new AiRecharge(); aiRecharge.setOrderNum(orderNo); aiRecharge.setUserId(SecurityUtils.getAiUserId()); aiRecharge.setAmount(amount); - aiRecharge.setGearId(req.getGearId()); + aiRecharge.setGearId(gearId); aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount()); aiRecharge.setSource(userInfo.getSource()); + AiRechargeGift aiRechargeGift = aiRechargeGiftService.selectAiRechargeGiftById(aiRechargeGiftGear.getRechargeId()); + aiRecharge.setPayType(aiRechargeGift.getPayType()); + aiRecharge.setPayUrl(payUrl); aiRechargeService.insertAiRecharge(aiRecharge); + PayResVO payResVO = new PayResVO(); payResVO.setOrderNo(orderNo); - payResVO.setPayUrl(jinShaBodyRes.getData().getPayurl()); + payResVO.setPayUrl(payUrl); return payResVO; } @Override @Transactional public AjaxResult jinShaCall(JinShaBodyCall req) { + log.info("jinsha支付回调,订单号: {}, 状态: {}", req.getOrderno(), req.getStatus()); + + // 验证签名 TreeMap params = new TreeMap<>(); params.put("status", req.getStatus()); params.put("orderno", req.getOrderno()); @@ -123,14 +167,58 @@ public class JinShaService implements IJinShaService { StringBuilder append = this.append(params); String sign = this.sign(append); if (!sign.equals(req.getSign())) { - log.error("支付回调签名错误 {}", req.getOrderno()); - return AjaxResult.error(); + log.error("jinsha支付回调签名错误,订单号: {}, 期望签名: {}, 实际签名: {}", + req.getOrderno(), sign, req.getSign()); + return AjaxResult.error("签名验证失败"); + } + + // 处理订单状态 + // 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(); } - //充值成功处理 - aiRechargeService.addRecharge(req.getOrderno()); - return AjaxResult.success(); } + /** + * 生成签名 + * 按照文档要求:key=value&key=value&...&secret=secret,然后MD5 + */ + private String generateSign(TreeMap sortedParams) { + StringBuilder paramSb = new StringBuilder(); + for (Map.Entry 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 sortedParams) { StringBuilder paramSb = new StringBuilder(); for (Map.Entry entry : sortedParams.entrySet()) { diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java index 5ed2d92..7f65af4 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java @@ -54,6 +54,8 @@ public class KaDaService implements IKaDaService { private IAiRechargeService aiRechargeService; @Autowired private IAiRechargeGiftService aiRechargeGiftService; + @Autowired + private IExchangeRateService exchangeRateService; @Override @@ -65,7 +67,8 @@ public class KaDaService implements IKaDaService { throw new ServiceException("The gear position does not exist.", -1); } 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 dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/YuZhouService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/YuZhouService.java index b12b677..40edd36 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/YuZhouService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/YuZhouService.java @@ -60,37 +60,111 @@ public class YuZhouService implements IYuZhouService { String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date()); 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 createMap = new HashMap<>(); createMap.put("merchantNo", appId); createMap.put("orderNo", orderNo); createMap.put("money", amount.toString()); - createMap.put("description", "test"); - createMap.put("name", "test"); - createMap.put("email", "test@gmail.com"); - createMap.put("callbackUrl", callbackUrl + "/api/pay/yuzhou-callBack"); - createMap.put("phone", "7383442114"); + createMap.put("description", "Recharge"); + createMap.put("email", email); + createMap.put("phone", phone); + createMap.put("notifyUrl", callbackUrl + "/api/pay/yuzhou-callBack"); 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); createMap.put("signature", signedStr); + String postStr = JSONUtil.toJsonStr(createMap); - String response = HttpRequest.post(url + "/gateway/order/US/payIn").header("Content-Type", "application/json") - .body(postStr).execute().body(); + log.info("yuzhou支付请求参数: {}", postStr); + + // 调用新的接口地址 + 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); Integer code = returnObj.getInt("code"); if (200 != code) { - log.error("yuzhou支付请求的响应异常 {}", returnObj); - throw new Exception("yuzhoupay responsebody is null"); + String msg = returnObj.getStr("msg"); + 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"); + if (data == null) { + log.error("yuzhou支付响应数据为空: {}", returnObj); + throw new ServiceException("yuzhoupay response data is null", -1); + } + String money = data.getStr("money"); 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.setOrderNum(orderNo); aiRecharge.setUserId(SecurityUtils.getAiUserId()); - aiRecharge.setAmount(new BigDecimal(money)); + aiRecharge.setAmount(new BigDecimal(money != null ? money : amount.toString())); aiRecharge.setGearId(gearId); aiRecharge.setSource(userInfo.getSource()); aiRecharge.setGearAmount(aiRechargeGiftGear.getRechargeAmount()); @@ -98,6 +172,7 @@ public class YuZhouService implements IYuZhouService { aiRecharge.setPayType(aiRechargeGift.getPayType()); aiRecharge.setPayUrl(payUrl); aiRechargeService.insertAiRecharge(aiRecharge); + PayResVO payResVO = new PayResVO(); payResVO.setOrderNo(orderNo); payResVO.setPayUrl(payUrl); @@ -106,31 +181,60 @@ public class YuZhouService implements IYuZhouService { @Override public String callBack(Map map) throws Exception { - int code = Integer.parseInt(map.get("status").toString()); - String orderNo = map.get("orderNo").toString(); - if (10 != code) { - log.error("yuzhou支付失败 {}", orderNo); - return null; + log.info("yuzhou支付回调接收参数: {}", map); + + // 验证必要参数 + if (map == null || map.isEmpty()) { + log.error("yuzhou支付回调参数为空"); + return "FAIL"; } -// Map createMap = new HashMap<>(); -// createMap.put("orderNo", orderNo); -// createMap.put("platOrderNo", map.get("platOrderNo")); -// createMap.put("money", map.get("money")); -// createMap.put("fee", map.get("fee")); -// createMap.put("status", map.get("status")); -// createMap.put("message", map.get("message")); - String signature = map.get("signature").toString(); + + Object statusObj = map.get("status"); + Object orderNoObj = map.get("orderNo"); + + if (statusObj == null || orderNoObj == null) { + log.error("yuzhou支付回调缺少必要参数: status={}, orderNo={}", statusObj, orderNoObj); + return "FAIL"; + } + + 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); - boolean b = RSAUtils.verifySign(entries, publicKey); - if (!b) { - log.error("yuzhou支付回调签名错误 {}", orderNo); - log.error("yuzhou支付回调签名 {}", signature); - log.error("yuzhou支付回调报文 {}", map); - return null; + boolean verifyResult = RSAUtils.verifySign(entries, publicKey); + if (!verifyResult) { + log.error("yuzhou支付回调签名验证失败 orderNo: {}", orderNo); + log.error("yuzhou支付回调签名: {}", signatureObj); + log.error("yuzhou支付回调完整报文: {}", map); + 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); + log.info("yuzhou支付回调处理成功 orderNo: {}", orderNo); + return "SUCCESS"; + } catch (Exception e) { + log.error("yuzhou支付回调处理失败 orderNo: {}", orderNo, e); + return "FAIL"; } - //充值成功处理 - aiRechargeService.addRecharge(orderNo); - return "SUCCESS"; } diff --git a/web-api/ruoyi-system/src/main/resources/mapper/system/AiOrderMapper.xml b/web-api/ruoyi-system/src/main/resources/mapper/system/AiOrderMapper.xml index c8a9b79..1951a01 100644 --- a/web-api/ruoyi-system/src/main/resources/mapper/system/AiOrderMapper.xml +++ b/web-api/ruoyi-system/src/main/resources/mapper/system/AiOrderMapper.xml @@ -21,10 +21,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + - 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 @@ -61,6 +69,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where result = #{result} + + @@ -118,7 +141,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" amount = #{amount}, result = #{result}, status = #{status}, - status = #{source}, + source = #{source}, text = #{text}, is_top = #{isTop}, diff --git a/web-api/ruoyi-system/src/main/resources/mapper/system/AiTemplateMapper.xml b/web-api/ruoyi-system/src/main/resources/mapper/system/AiTemplateMapper.xml index afb0aa7..9ca60f9 100644 --- a/web-api/ruoyi-system/src/main/resources/mapper/system/AiTemplateMapper.xml +++ b/web-api/ruoyi-system/src/main/resources/mapper/system/AiTemplateMapper.xml @@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -20,7 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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 @@ -46,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" chinese_content, english_content, image_url, + ai_id, status, remark, create_time, @@ -59,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{chineseContent}, #{englishContent}, #{imageUrl}, + #{aiId}, #{status}, #{remark}, #{createTime}, @@ -76,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" chinese_content = #{chineseContent}, english_content = #{englishContent}, image_url = #{imageUrl}, + ai_id = #{aiId}, status = #{status}, remark = #{remark}, create_time = #{createTime}, diff --git a/web-api/ruoyi-system/src/main/resources/mapper/system/AiUserMapper.xml b/web-api/ruoyi-system/src/main/resources/mapper/system/AiUserMapper.xml index 0c0e8e0..5af6704 100644 --- a/web-api/ruoyi-system/src/main/resources/mapper/system/AiUserMapper.xml +++ b/web-api/ruoyi-system/src/main/resources/mapper/system/AiUserMapper.xml @@ -33,15 +33,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + - 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 + + @@ -108,6 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" source, ip, country, + dept_id, #{delFlag}, @@ -134,6 +145,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{userId}, #{source}, #{country}, + #{deptId}, @@ -163,8 +175,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" superior_id = #{superiorId}, user_id = #{userId}, source = #{source}, - source = #{ip}, + ip = #{ip}, country = #{country}, + dept_id = #{deptId}, where id = #{id} diff --git a/web-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/web-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml index bd232c8..bcf5ee0 100644 --- a/web-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml +++ b/web-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -13,6 +13,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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 @@ -59,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"