From 349f1c037495b8237406bf57fbe0f17c4c3ea113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Wed, 29 Jul 2026 14:34:54 +0800 Subject: [PATCH] feat: add dashboard and virtual pay enhancements --- docs/virtual-pay-setup.md | 22 +- .../app/AppDashboardController.java | 28 ++ .../app/AppIntegralRecordController.java | 19 ++ .../app/AppVirtualPayController.java | 18 ++ .../src/main/resources/application.yml | 12 +- ruoyi-admin/src/main/resources/logback.xml | 4 +- ruoyi-office/pom.xml | 14 +- .../controller/TtCopyTemplateController.java | 5 +- .../controller/TtProjectInfoController.java | 15 + .../domain/ProjectLinkImportDetail.java | 71 ++++ .../domain/ProjectLinkImportResult.java | 98 ++++++ .../office/domain/ProjectLinkImportRow.java | 67 ++++ .../java/com/ruoyi/office/domain/TtCode.java | 49 ++- .../office/mapper/TtProjectInfoMapper.java | 5 + .../mapper/TtProjectLinkCheckMapper.java | 4 + .../office/service/ITtProjectInfoService.java | 11 + .../service/impl/TtCodeServiceImpl.java | 4 +- .../impl/TtProjectInfoServiceImpl.java | 305 +++++++++++++++++- .../importer/ProjectLinkImportParser.java | 287 ++++++++++++++++ .../resources/mapper/office/TtCodeMapper.xml | 20 +- .../mapper/office/TtProjectInfoMapper.xml | 12 + .../office/TtProjectLinkCheckMapper.xml | 6 + .../impl/TtProjectInfoServiceImplTest.java | 112 +++++++ .../importer/ProjectLinkImportParserTest.java | 89 +++++ .../ruoyi/app/domain/AppDashboardSummary.java | 108 +++++++ .../com/ruoyi/app/domain/AppResourceList.java | 60 ++++ .../com/ruoyi/app/domain/AppVirtualOrder.java | 26 ++ .../app/domain/AppVirtualOrderSummary.java | 131 ++++++++ .../request/CreateVirtualOrderRequest.java | 16 +- .../ruoyi/app/mapper/AppDashboardMapper.java | 11 + .../app/mapper/AppIntegralRecordMapper.java | 3 + .../ruoyi/app/mapper/AppResourceMapper.java | 25 ++ .../app/mapper/AppVirtualOrderMapper.java | 15 +- .../app/service/IAppDashboardService.java | 11 + .../app/service/IAppVirtualPayService.java | 6 + .../service/impl/AppDashboardServiceImpl.java | 23 ++ .../service/impl/AppResourceServiceImpl.java | 189 ++++++++++- .../impl/AppVirtualPayServiceImpl.java | 206 +++++++++--- .../mapper/app/AppDashboardMapper.xml | 78 +++++ .../mapper/app/AppIntegralRecordMapper.xml | 9 +- .../mapper/app/AppPayOrderMapper.xml | 11 +- .../mapper/app/AppResourceMapper.xml | 165 ++++++++-- .../mapper/app/AppVirtualOrderMapper.xml | 93 +++++- ruoyi-ui/src/api/app/dashboard.js | 9 + ruoyi-ui/src/views/app/resource/index.vue | 135 ++++++-- ruoyi-ui/src/views/app/virtualOrder/index.vue | 3 + ruoyi-ui/src/views/index.vue | 212 +++++++++++- ruoyi-ui/src/views/office/code/index.vue | 99 ++---- .../src/views/office/copyTemplate/index.vue | 12 +- ruoyi-ui/src/views/office/project/index.vue | 245 +++++++++++++- sql/copy_template.sql | 8 +- sql/dashboard_statistics_indexes.sql | 46 +++ sql/source_code_technology_fields.sql | 7 + sql/virtual_pay_order_guard.sql | 30 ++ sql/virtual_pay_resource_specs.sql | 29 ++ 55 files changed, 3085 insertions(+), 213 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppDashboardController.java create mode 100644 ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportDetail.java create mode 100644 ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportResult.java create mode 100644 ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportRow.java create mode 100644 ruoyi-office/src/main/java/com/ruoyi/office/service/importer/ProjectLinkImportParser.java create mode 100644 ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImplTest.java create mode 100644 ruoyi-office/src/test/java/com/ruoyi/office/service/importer/ProjectLinkImportParserTest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/app/domain/AppDashboardSummary.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/app/domain/AppVirtualOrderSummary.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/app/mapper/AppDashboardMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/app/service/IAppDashboardService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/app/service/impl/AppDashboardServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/app/AppDashboardMapper.xml create mode 100644 ruoyi-ui/src/api/app/dashboard.js create mode 100644 sql/dashboard_statistics_indexes.sql create mode 100644 sql/source_code_technology_fields.sql create mode 100644 sql/virtual_pay_order_guard.sql create mode 100644 sql/virtual_pay_resource_specs.sql diff --git a/docs/virtual-pay-setup.md b/docs/virtual-pay-setup.md index 54dbfe9..1f86d6e 100644 --- a/docs/virtual-pay-setup.md +++ b/docs/virtual-pay-setup.md @@ -4,13 +4,24 @@ ## 1. 执行数据库迁移 -执行 `sql/virtual_pay_resource.sql`。脚本会: +首次部署依次执行: + +1. `sql/virtual_pay_resource.sql` +2. `sql/virtual_pay_resource_specs.sql` +3. `sql/virtual_pay_order_guard.sql` + +第二个脚本把每条资源下载项改造成可单独购买的规格,第三个脚本阻止同一用户对同一规格同时产生多笔待支付或已支付订单。已经执行前两个脚本的环境只需补执行第三个脚本。 + +脚本会: - 为资源增加分单位价格 `price_fen`; +- 为每条资源下载项增加规格价格、状态和排序; - 创建价格档位表 `app_virtual_product`; - 创建虚拟支付订单表 `app_virtual_order`; - 创建资源访问权益表 `app_resource_entitlement`; +- 为待支付/已支付订单增加数据库唯一防重键; - 将已有 `is_ad=3` 资源的 `ad_number`(元)迁移为 `price_fen`(分)。 +- 旧订单与权益保留整项解锁能力,新订单按下载项规格解锁。 ## 2. 配置虚拟支付环境变量 @@ -38,11 +49,16 @@ AppKey 和小程序 Secret 不应提交到 Git,生产环境应由部署平台 然后在若依后台编辑付费资源: - 获取方式选择“付费”; -- 价格填写分,例如 5 元填写 `500`; -- 同一价格的资源填写同一个已发布 `productId`。 +- 在“资源规格”中按版本从低到高填写排序,例如源码版 `1`、文档版 `2`、部署版 `3`; +- 高排序版本自动包含所有低排序版本,且版本价格必须随排序递增; +- 为每个版本填写价格,例如 5 元填写 `500`; +- 规格价格必须存在于 `app_virtual_product` 的已启用价格档位中,系统会自动匹配对应的 `productId`。 +- 所有可能产生的升级差价也必须配置价格档位。例如版本价格分别为 `9900`、`19900`、`29900` 分,还需配置 `10000`、`20000` 分两个差价档位。 后台会自动维护“价格 -> productId”唯一映射;一个 productId 不能绑定多个价格。 +用户升级时,服务端按“目标版本当前价格 - 已拥有最高版本当前价格”计算实付金额。前端展示金额仅供参考,签名和订单金额始终由服务端重新计算。高版本退款后,只撤销该高版本订单对应的权益,用户更早单独购买的低版本权益仍然保留。 + ## 4. 配置消息推送 在小程序后台配置: diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppDashboardController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppDashboardController.java new file mode 100644 index 0000000..4ba11b5 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppDashboardController.java @@ -0,0 +1,28 @@ +package com.ruoyi.web.controller.app; + +import com.ruoyi.app.service.IAppDashboardService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 首页业务指标接口。 + */ +@RestController +@RequestMapping("/app/dashboard") +public class AppDashboardController extends BaseController +{ + @Autowired + private IAppDashboardService appDashboardService; + + @PreAuthorize("@ss.hasAnyPermi('app:virtualOrder:list,app:order:list,system:user:list')") + @GetMapping("/summary") + public AjaxResult summary() + { + return success(appDashboardService.selectDashboardSummary()); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppIntegralRecordController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppIntegralRecordController.java index 53342e9..2913bc8 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppIntegralRecordController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppIntegralRecordController.java @@ -15,6 +15,7 @@ import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.PermissionService; import com.ruoyi.system.mapper.SysUserMapper; import com.ruoyi.system.service.ISysConfigService; import org.springframework.beans.factory.annotation.Autowired; @@ -46,6 +47,8 @@ public class AppIntegralRecordController extends BaseController private SysUserMapper sysUserMapper; @Resource private AppResourceMapper appResourceMapper; + @Resource + private PermissionService permissionService; @Autowired private IAppLotteryGoodsService appLotteryGoodsService; @Autowired @@ -54,14 +57,30 @@ public class AppIntegralRecordController extends BaseController /** * 查询积分记录列表 */ + @PreAuthorize("isAuthenticated()") @GetMapping("/list") public TableDataInfo list(AppIntegralRecord appIntegralRecord) { startPage(); + if (!permissionService.hasPermi("app:appIntegra:list")) + { + return getDataTable(appIntegralRecordMapper.selectMyIntegralRecordList(getUserId())); + } List list = appIntegralRecordService.selectAppIntegralRecordList(appIntegralRecord); return getDataTable(list); } + /** + * 小程序“积分记录”,只查询当前登录用户的积分增减,不包含现金购买记录。 + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/my") + public TableDataInfo myList() + { + startPage(); + return getDataTable(appIntegralRecordMapper.selectMyIntegralRecordList(getUserId())); + } + /** * 导出积分记录列表 */ diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppVirtualPayController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppVirtualPayController.java index bacec19..6cfbcc0 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppVirtualPayController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/app/AppVirtualPayController.java @@ -6,11 +6,13 @@ import com.ruoyi.app.domain.request.CreateVirtualOrderRequest; import com.ruoyi.app.service.IAppVirtualPayService; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; 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; @@ -44,6 +46,14 @@ public class AppVirtualPayController extends BaseController return success(virtualPayService.createOrder(request)); } + @PreAuthorize("isAuthenticated()") + @GetMapping("/resource-orders") + public TableDataInfo listMyOrders() + { + startPage(); + return getDataTable(virtualPayService.listMyOrders()); + } + @PreAuthorize("isAuthenticated()") @GetMapping("/resource-orders/{orderNo}") public AjaxResult queryOrder(@PathVariable String orderNo, @@ -52,6 +62,14 @@ public class AppVirtualPayController extends BaseController return success(virtualPayService.queryOrder(orderNo, sync)); } + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/resource-orders/{orderNo}") + public AjaxResult cancelOrder(@PathVariable String orderNo) + { + virtualPayService.cancelOrder(orderNo); + return success("订单已取消"); + } + /** * 在小程序后台保存消息推送配置时使用。 */ diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index baa1af9..8a5fd8d 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -9,8 +9,8 @@ ruoyi: # 实例演示开关 demoEnabled: true # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) - profile: /home/upload -# profilee: D:/ruoyi/uploadPath +# profile: /home/upload + profilee: D:/ruoyi/uploadPath # 获取ip地址开关 addressEnabled: false # 验证码类型 math 数字计算 char 字符验证 @@ -42,10 +42,10 @@ wx: appid: wx17b46f75e762c184 # 微信小程序appid secret: 1fa844dc33e70f7d813f24cd2af7678b # 微信小程序密钥 merchantId: 1704387455 # 商户号 -# privateKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\apiclient_key.pem # 商户API私钥路径(测试环境) -# publicKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\pub_key.pem # 商户API公钥路径(测试环境) - privateKeyPath: /home/cert/apiclient_key.pem # 商户API私钥路径(正式环境) - publicKeyPath: /home/cert/pub_key.pem # 商户API公钥路径(正式环境) + privateKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\apiclient_key.pem # 商户API私钥路径(测试环境) + publicKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\pub_key.pem # 商户API公钥路径(测试环境) +# privateKeyPath: /home/cert/apiclient_key.pem # 商户API私钥路径(正式环境) +# publicKeyPath: /home/cert/pub_key.pem # 商户API公钥路径(正式环境) publicKeyId: PUB_KEY_ID_0117043874552025011100188700000234 merchantSerialNumber: 743FBCB9F5DFD76104A468C6AC6EDD41268634A3 # 商户API证书序列号 apiV3Key: G7kL2mN8pQ4rT1vX9yZ3bC5dF6hJ0sW1 # 商户APIV3密钥 diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml index bd056c4..6fb741e 100644 --- a/ruoyi-admin/src/main/resources/logback.xml +++ b/ruoyi-admin/src/main/resources/logback.xml @@ -1,8 +1,8 @@ - - + + diff --git a/ruoyi-office/pom.xml b/ruoyi-office/pom.xml index b993802..b1f67a0 100644 --- a/ruoyi-office/pom.xml +++ b/ruoyi-office/pom.xml @@ -28,6 +28,18 @@ ruoyi-system + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + 4.13.2 + test + + - \ No newline at end of file + diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java index 4a7b1af..a618a68 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java @@ -174,10 +174,13 @@ public class TtCopyTemplateController extends BaseController { content = content.replace("{projectName}", projectName); content = content.replace("{codeDesc}", plainDesc); content = content.replace("{codeEnvironment}", code.getCodeEnvironment() != null ? code.getCodeEnvironment() : ""); + content = content.replace("{frontendTechnology}", code.getFrontendTechnology() != null ? code.getFrontendTechnology() : ""); + content = content.replace("{backendTechnology}", code.getBackendTechnology() != null ? code.getBackendTechnology() : ""); + content = content.replace("{databaseTechnology}", code.getDatabaseTechnology() != null ? code.getDatabaseTechnology() : ""); content = content.replace("{codeTechnology}", code.getCodeTechnology() != null ? code.getCodeTechnology() : ""); content = content.replace("{diskLink}", code.getDiskLink() != null ? code.getDiskLink() : ""); content = content.replace("{screenshots}", screenshots); return success(content); } -} \ No newline at end of file +} diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtProjectInfoController.java b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtProjectInfoController.java index e2c1cf6..2a47905 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtProjectInfoController.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtProjectInfoController.java @@ -8,6 +8,7 @@ import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import com.ruoyi.common.utils.CoverGenerator; +import com.ruoyi.office.domain.ProjectLinkImportResult; import com.ruoyi.office.domain.TtCode; import com.ruoyi.office.service.ITtCodeService; import com.ruoyi.office.service.IProjectLinkCheckService; @@ -20,7 +21,9 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; 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; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -70,6 +73,18 @@ public class TtProjectInfoController extends BaseController { util.exportExcel(response, list, "项目清单数据"); } + /** + * 按项目名称导入夸克或百度网盘链接。 + */ + @PreAuthorize("@ss.hasPermi('office:project:edit')") + @Log(title = "导入项目网盘链接", businessType = BusinessType.IMPORT) + @PostMapping("/import-links") + public AjaxResult importLinks(@RequestParam("file") MultipartFile file, + @RequestParam("diskType") String diskType) { + ProjectLinkImportResult result = ttProjectInfoService.importProjectLinks(file, diskType); + return AjaxResult.success(result.buildMessage(), result); + } + /** * 获取项目清单详细信息 */ diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportDetail.java b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportDetail.java new file mode 100644 index 0000000..f0e3de8 --- /dev/null +++ b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportDetail.java @@ -0,0 +1,71 @@ +package com.ruoyi.office.domain; + +/** + * 项目网盘链接导入明细。 + */ +public class ProjectLinkImportDetail +{ + /** 原文件中的行号。 */ + private int rowNumber; + + /** 项目名称。 */ + private String projectName; + + /** SKIPPED / FAILED。 */ + private String resultType; + + /** 跳过或失败原因。 */ + private String reason; + + public ProjectLinkImportDetail() + { + } + + public ProjectLinkImportDetail(int rowNumber, String projectName, String resultType, String reason) + { + this.rowNumber = rowNumber; + this.projectName = projectName; + this.resultType = resultType; + this.reason = reason; + } + + public int getRowNumber() + { + return rowNumber; + } + + public void setRowNumber(int rowNumber) + { + this.rowNumber = rowNumber; + } + + public String getProjectName() + { + return projectName; + } + + public void setProjectName(String projectName) + { + this.projectName = projectName; + } + + public String getResultType() + { + return resultType; + } + + public void setResultType(String resultType) + { + this.resultType = resultType; + } + + public String getReason() + { + return reason; + } + + public void setReason(String reason) + { + this.reason = reason; + } +} diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportResult.java b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportResult.java new file mode 100644 index 0000000..4da31ed --- /dev/null +++ b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportResult.java @@ -0,0 +1,98 @@ +package com.ruoyi.office.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * 项目网盘链接导入结果。 + */ +public class ProjectLinkImportResult +{ + private int totalCount; + + private int addedCount; + + private int updatedCount; + + private int unchangedCount; + + private int skippedCount; + + private int failedCount; + + private final List details = new ArrayList<>(); + + public void addSkipped(int rowNumber, String projectName, String reason) + { + skippedCount++; + details.add(new ProjectLinkImportDetail(rowNumber, projectName, "SKIPPED", reason)); + } + + public void addFailed(int rowNumber, String projectName, String reason) + { + failedCount++; + details.add(new ProjectLinkImportDetail(rowNumber, projectName, "FAILED", reason)); + } + + public String buildMessage() + { + return "导入完成:共 " + totalCount + " 条,新增 " + addedCount + " 条,更新 " + + updatedCount + " 条,未变化 " + unchangedCount + " 条,跳过 " + + skippedCount + " 条,失败 " + failedCount + " 条"; + } + + public int getTotalCount() + { + return totalCount; + } + + public void setTotalCount(int totalCount) + { + this.totalCount = totalCount; + } + + public int getAddedCount() + { + return addedCount; + } + + public void incrementAddedCount() + { + addedCount++; + } + + public int getUpdatedCount() + { + return updatedCount; + } + + public void incrementUpdatedCount() + { + updatedCount++; + } + + public int getUnchangedCount() + { + return unchangedCount; + } + + public void incrementUnchangedCount() + { + unchangedCount++; + } + + public int getSkippedCount() + { + return skippedCount; + } + + public int getFailedCount() + { + return failedCount; + } + + public List getDetails() + { + return details; + } +} diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportRow.java b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportRow.java new file mode 100644 index 0000000..d9131c0 --- /dev/null +++ b/ruoyi-office/src/main/java/com/ruoyi/office/domain/ProjectLinkImportRow.java @@ -0,0 +1,67 @@ +package com.ruoyi.office.domain; + +/** + * 项目网盘链接导入文件中的一行原始数据。 + */ +public class ProjectLinkImportRow +{ + private int rowNumber; + + private String projectName; + + private String shareAddress; + + private String extractCode; + + private String shareStatus; + + public int getRowNumber() + { + return rowNumber; + } + + public void setRowNumber(int rowNumber) + { + this.rowNumber = rowNumber; + } + + public String getProjectName() + { + return projectName; + } + + public void setProjectName(String projectName) + { + this.projectName = projectName; + } + + public String getShareAddress() + { + return shareAddress; + } + + public void setShareAddress(String shareAddress) + { + this.shareAddress = shareAddress; + } + + public String getExtractCode() + { + return extractCode; + } + + public void setExtractCode(String extractCode) + { + this.extractCode = extractCode; + } + + public String getShareStatus() + { + return shareStatus; + } + + public void setShareStatus(String shareStatus) + { + this.shareStatus = shareStatus; + } +} diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/domain/TtCode.java b/ruoyi-office/src/main/java/com/ruoyi/office/domain/TtCode.java index c9b300a..453a373 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/domain/TtCode.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/domain/TtCode.java @@ -40,11 +40,29 @@ public class TtCode extends BaseEntity { private String codeEnvironment; /** - * 项目技术 + * 其他技术 */ - @Excel(name = "项目技术") + @Excel(name = "其他技术") private String codeTechnology; + /** + * 前端 + */ + @Excel(name = "前端") + private String frontendTechnology; + + /** + * 后端 + */ + @Excel(name = "后端") + private String backendTechnology; + + /** + * 数据库 + */ + @Excel(name = "数据库") + private String databaseTechnology; + /** * 来源 */ @@ -123,6 +141,30 @@ public class TtCode extends BaseEntity { return codeTechnology; } + public void setFrontendTechnology(String frontendTechnology) { + this.frontendTechnology = frontendTechnology; + } + + public String getFrontendTechnology() { + return frontendTechnology; + } + + public void setBackendTechnology(String backendTechnology) { + this.backendTechnology = backendTechnology; + } + + public String getBackendTechnology() { + return backendTechnology; + } + + public void setDatabaseTechnology(String databaseTechnology) { + this.databaseTechnology = databaseTechnology; + } + + public String getDatabaseTechnology() { + return databaseTechnology; + } + public void setCodeSource(String codeSource) { this.codeSource = codeSource; } @@ -187,6 +229,9 @@ public class TtCode extends BaseEntity { .append("codeDesc", getCodeDesc()) .append("codeEnvironment", getCodeEnvironment()) .append("codeTechnology", getCodeTechnology()) + .append("frontendTechnology", getFrontendTechnology()) + .append("backendTechnology", getBackendTechnology()) + .append("databaseTechnology", getDatabaseTechnology()) .append("codeSource", getCodeSource()) .append("paymentType", getPaymentType()) .append("diskLink", getDiskLink()) diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java index 0ee8feb..f016989 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java @@ -2,6 +2,7 @@ package com.ruoyi.office.mapper; import java.util.List; import com.ruoyi.office.domain.TtProjectInfo; +import org.apache.ibatis.annotations.Param; /** * 项目清单Mapper接口 @@ -62,4 +63,8 @@ public interface TtProjectInfoMapper List lastUpdateList(String searchKey); TtProjectInfo selectTtProjectInfoByName(String codeName); + + int updateProjectQuarkUrl(@Param("id") Integer id, @Param("projectUrl") String projectUrl); + + int updateProjectBaiduUrl(@Param("id") Integer id, @Param("projectBaiduUrl") String projectBaiduUrl); } diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectLinkCheckMapper.java b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectLinkCheckMapper.java index b975219..b469784 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectLinkCheckMapper.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectLinkCheckMapper.java @@ -1,6 +1,7 @@ package com.ruoyi.office.mapper; import com.ruoyi.office.domain.TtProjectLinkCheck; +import org.apache.ibatis.annotations.Param; /** * 项目网盘链接检测结果 Mapper。 @@ -12,4 +13,7 @@ public interface TtProjectLinkCheckMapper int deleteByProjectId(Integer projectId); int deleteByProjectIds(Integer[] projectIds); + + int deleteByProjectIdAndDiskType(@Param("projectId") Integer projectId, + @Param("diskType") String diskType); } diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtProjectInfoService.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtProjectInfoService.java index e1a897a..07a310c 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtProjectInfoService.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtProjectInfoService.java @@ -2,6 +2,8 @@ package com.ruoyi.office.service; import java.util.List; import com.ruoyi.office.domain.TtProjectInfo; +import com.ruoyi.office.domain.ProjectLinkImportResult; +import org.springframework.web.multipart.MultipartFile; /** * 项目清单Service接口 @@ -64,4 +66,13 @@ public interface ITtProjectInfoService List lastUpdateList(String searchKey); TtProjectInfo selectTtProjectInfoByName(String codeName); + + /** + * 导入夸克或百度网盘分享链接。 + * + * @param file 网盘客户端导出的 CSV/Excel 文件 + * @param diskType QUARK/BAIDU + * @return 导入结果 + */ + ProjectLinkImportResult importProjectLinks(MultipartFile file, String diskType); } diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java index e603e17..55ed399 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java @@ -149,7 +149,7 @@ public class TtCodeServiceImpl implements ITtCodeService { content.append(ttCode.getCodeDesc()); content.append("

### 运行环境

"); content.append(ttCode.getCodeEnvironment()); - content.append("

### 项目技术

"); + content.append("

### 其他技术

"); content.append(ttCode.getCodeTechnology()); ttArticles.setContent(content.toString()); ttArticlesMapper.insertTtArticles(ttArticles); @@ -168,7 +168,7 @@ public class TtCodeServiceImpl implements ITtCodeService { content.append(ttCode.getCodeDesc()); content.append("

### 运行环境

"); content.append(ttCode.getCodeEnvironment()); - content.append("

### 项目技术

"); + content.append("

### 其他技术

"); content.append(ttCode.getCodeTechnology()); content.append("

### 演示视频

"); content.append("请移步首页-视频资源,搜索项目编号查看"); diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java index b587c1e..048d86b 100644 --- a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java +++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java @@ -1,15 +1,30 @@ package com.ruoyi.office.service.impl; -import java.util.Collections; +import java.net.URI; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.office.domain.ProjectLinkImportResult; +import com.ruoyi.office.domain.ProjectLinkImportRow; import com.ruoyi.office.domain.TtCode; import com.ruoyi.office.mapper.TtCodeMapper; import com.ruoyi.office.mapper.TtProjectLinkCheckMapper; +import com.ruoyi.office.service.importer.ProjectLinkImportParser; +import com.ruoyi.office.service.netdisk.NetDiskConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.office.mapper.TtProjectInfoMapper; import com.ruoyi.office.domain.TtProjectInfo; import com.ruoyi.office.service.ITtProjectInfoService; @@ -23,12 +38,23 @@ import com.ruoyi.office.service.ITtProjectInfoService; @Service public class TtProjectInfoServiceImpl implements ITtProjectInfoService { + private static final Pattern PROJECT_NUM_PATTERN = + Pattern.compile("^[【\\[]\\s*(S\\d+)\\s*[】\\]]", Pattern.CASE_INSENSITIVE); + + private static final Pattern QUARK_URL_PATTERN = + Pattern.compile("https://pan\\.quark\\.cn/s/[A-Za-z0-9_-]+(?:\\?[^\\s\"'<>,。;]*)?"); + + private static final Pattern BAIDU_PWD_PATTERN = + Pattern.compile("(?:\\?|&)pwd=[^&]*", Pattern.CASE_INSENSITIVE); + @Autowired private TtProjectInfoMapper ttProjectInfoMapper; @Autowired private TtCodeMapper ttCodeMapper; @Autowired private TtProjectLinkCheckMapper ttProjectLinkCheckMapper; + @Autowired + private ProjectLinkImportParser projectLinkImportParser; /** * 查询项目清单 @@ -129,4 +155,281 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService public TtProjectInfo selectTtProjectInfoByName(String codeName) { return ttProjectInfoMapper.selectTtProjectInfoByName(codeName); } + + /** + * 按项目名称导入网盘链接。新项目同时写入项目名称和源码名称; + * 已存在项目只更新本次导入的网盘链接字段。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ProjectLinkImportResult importProjectLinks(MultipartFile file, String diskType) + { + String normalizedDiskType = normalizeDiskType(diskType); + List sourceRows = projectLinkImportParser.parse(file, normalizedDiskType); + ProjectLinkImportResult result = new ProjectLinkImportResult(); + result.setTotalCount(sourceRows.size()); + + List validRows = validateRows(sourceRows, normalizedDiskType, result); + Set duplicateNames = findDuplicateNames(validRows); + Map> projectsByName = loadProjectsByNormalizedName(); + + for (ValidatedImportRow row : validRows) + { + if (duplicateNames.contains(row.normalizedName)) + { + result.addFailed(row.rowNumber, row.projectName, "导入文件中存在重复项目名称"); + continue; + } + + List matches = projectsByName.get(row.normalizedName); + if (matches != null && matches.size() > 1) + { + result.addFailed(row.rowNumber, row.projectName, "数据库中存在多个同名项目,无法确定更新目标"); + continue; + } + + if (matches == null || matches.isEmpty()) + { + TtProjectInfo project = createProject(row, normalizedDiskType); + if (ttProjectInfoMapper.insertTtProjectInfo(project) != 1) + { + throw new ServiceException("新增项目失败:" + row.projectName); + } + List inserted = new ArrayList<>(); + inserted.add(project); + projectsByName.put(row.normalizedName, inserted); + result.incrementAddedCount(); + continue; + } + + TtProjectInfo project = matches.get(0); + String oldLink = NetDiskConstants.DISK_QUARK.equals(normalizedDiskType) + ? project.getProjectUrl() : project.getProjectBaiduUrl(); + if (StringUtils.equals(StringUtils.trimToEmpty(oldLink), row.linkUrl)) + { + result.incrementUnchangedCount(); + continue; + } + + int updated; + if (NetDiskConstants.DISK_QUARK.equals(normalizedDiskType)) + { + updated = ttProjectInfoMapper.updateProjectQuarkUrl(project.getId(), row.linkUrl); + project.setProjectUrl(row.linkUrl); + } + else + { + updated = ttProjectInfoMapper.updateProjectBaiduUrl(project.getId(), row.linkUrl); + project.setProjectBaiduUrl(row.linkUrl); + } + if (updated != 1) + { + throw new ServiceException("更新项目链接失败:" + row.projectName); + } + ttProjectLinkCheckMapper.deleteByProjectIdAndDiskType(project.getId(), normalizedDiskType); + result.incrementUpdatedCount(); + } + return result; + } + + private List validateRows(List sourceRows, + String diskType, + ProjectLinkImportResult result) + { + List validRows = new ArrayList<>(); + String successStatus = NetDiskConstants.DISK_QUARK.equals(diskType) ? "成功" : "生成成功"; + for (ProjectLinkImportRow sourceRow : sourceRows) + { + String projectName = normalizeProjectName(sourceRow.getProjectName()); + String status = StringUtils.trimToEmpty(sourceRow.getShareStatus()); + if (!successStatus.equals(status)) + { + result.addSkipped(sourceRow.getRowNumber(), projectName, + "分享状态不是" + successStatus + ":" + StringUtils.defaultIfBlank(status, "空")); + continue; + } + if (StringUtils.isBlank(projectName)) + { + result.addFailed(sourceRow.getRowNumber(), "", "项目名称不能为空"); + continue; + } + if (projectName.length() > 255) + { + result.addFailed(sourceRow.getRowNumber(), projectName, "项目名称超过 255 个字符"); + continue; + } + + try + { + String linkUrl = buildAndValidateLink(sourceRow, diskType); + validRows.add(new ValidatedImportRow(sourceRow.getRowNumber(), projectName, + normalizeProjectName(projectName), linkUrl)); + } + catch (IllegalArgumentException e) + { + result.addFailed(sourceRow.getRowNumber(), projectName, e.getMessage()); + } + } + return validRows; + } + + private String buildAndValidateLink(ProjectLinkImportRow row, String diskType) + { + String linkUrl; + if (NetDiskConstants.DISK_QUARK.equals(diskType)) + { + Matcher matcher = QUARK_URL_PATTERN.matcher(StringUtils.defaultString(row.getShareAddress())); + if (!matcher.find()) + { + throw new IllegalArgumentException("未找到有效的夸克网盘链接"); + } + linkUrl = matcher.group(); + } + else + { + linkUrl = StringUtils.trimToEmpty(row.getShareAddress()); + String extractCode = StringUtils.trimToEmpty(row.getExtractCode()); + if (StringUtils.isNotBlank(extractCode) && !BAIDU_PWD_PATTERN.matcher(linkUrl).find()) + { + try + { + linkUrl += linkUrl.contains("?") ? "&" : "?"; + linkUrl += "pwd=" + URLEncoder.encode(extractCode, "UTF-8"); + } + catch (Exception e) + { + throw new IllegalArgumentException("百度网盘提取码处理失败"); + } + } + } + + if (linkUrl.length() > 1000) + { + throw new IllegalArgumentException("网盘链接超过 1000 个字符"); + } + validateLinkHostAndPath(linkUrl, diskType); + return linkUrl; + } + + private void validateLinkHostAndPath(String linkUrl, String diskType) + { + try + { + URI uri = new URI(linkUrl); + String host = uri.getHost(); + String path = uri.getPath(); + boolean isHttps = "https".equalsIgnoreCase(uri.getScheme()); + boolean isQuark = NetDiskConstants.DISK_QUARK.equals(diskType) + && "pan.quark.cn".equalsIgnoreCase(host) + && path != null && path.startsWith("/s/"); + boolean isBaidu = NetDiskConstants.DISK_BAIDU.equals(diskType) + && host != null + && ("pan.baidu.com".equalsIgnoreCase(host) || "yun.baidu.com".equalsIgnoreCase(host)) + && path != null + && (path.startsWith("/s/") || path.startsWith("/share/")); + if (!isHttps || uri.getUserInfo() != null || (uri.getPort() != -1 && uri.getPort() != 443) + || (!isQuark && !isBaidu)) + { + throw new IllegalArgumentException("网盘链接格式不正确"); + } + } + catch (IllegalArgumentException e) + { + throw e; + } + catch (Exception e) + { + throw new IllegalArgumentException("网盘链接格式不正确"); + } + } + + private Map> loadProjectsByNormalizedName() + { + Map> projectsByName = new HashMap<>(); + List projects = ttProjectInfoMapper.selectTtProjectInfoList(new TtProjectInfo()); + for (TtProjectInfo project : projects) + { + String normalizedName = normalizeProjectName(project.getProjectName()); + if (StringUtils.isBlank(normalizedName)) + { + continue; + } + projectsByName.computeIfAbsent(normalizedName, key -> new ArrayList<>()).add(project); + } + return projectsByName; + } + + private Set findDuplicateNames(List rows) + { + Set names = new HashSet<>(); + Set duplicateNames = new HashSet<>(); + for (ValidatedImportRow row : rows) + { + if (!names.add(row.normalizedName)) + { + duplicateNames.add(row.normalizedName); + } + } + return duplicateNames; + } + + private TtProjectInfo createProject(ValidatedImportRow row, String diskType) + { + TtProjectInfo project = new TtProjectInfo(); + project.setProjectName(row.projectName); + project.setProjectName1(row.projectName); + project.setProjectNum(extractProjectNum(row.projectName)); + if (NetDiskConstants.DISK_QUARK.equals(diskType)) + { + project.setProjectUrl(row.linkUrl); + } + else + { + project.setProjectBaiduUrl(row.linkUrl); + } + return project; + } + + private String extractProjectNum(String projectName) + { + Matcher matcher = PROJECT_NUM_PATTERN.matcher(projectName); + return matcher.find() ? matcher.group(1).toUpperCase(Locale.ROOT) : null; + } + + private String normalizeProjectName(String projectName) + { + return StringUtils.trimToEmpty(projectName) + .replace("\uFEFF", "") + .replace('\u00A0', ' ') + .trim(); + } + + private String normalizeDiskType(String diskType) + { + String value = StringUtils.trimToEmpty(diskType).toUpperCase(Locale.ROOT); + if (!NetDiskConstants.DISK_QUARK.equals(value) && !NetDiskConstants.DISK_BAIDU.equals(value)) + { + throw new ServiceException("不支持的网盘类型"); + } + return value; + } + + private static class ValidatedImportRow + { + private final int rowNumber; + + private final String projectName; + + private final String normalizedName; + + private final String linkUrl; + + private ValidatedImportRow(int rowNumber, String projectName, String normalizedName, String linkUrl) + { + this.rowNumber = rowNumber; + this.projectName = projectName; + this.normalizedName = normalizedName; + this.linkUrl = linkUrl; + } + } } diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/importer/ProjectLinkImportParser.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/importer/ProjectLinkImportParser.java new file mode 100644 index 0000000..76bd212 --- /dev/null +++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/importer/ProjectLinkImportParser.java @@ -0,0 +1,287 @@ +package com.ruoyi.office.service.importer; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import cn.hutool.core.text.csv.CsvData; +import cn.hutool.core.text.csv.CsvReadConfig; +import cn.hutool.core.text.csv.CsvRow; +import cn.hutool.core.text.csv.CsvUtil; +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelUtil; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.office.domain.ProjectLinkImportRow; +import com.ruoyi.office.service.netdisk.NetDiskConstants; +import org.apache.commons.io.FilenameUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +/** + * 解析夸克、百度网盘客户端导出的 CSV 或 Excel 文件。 + */ +@Component +public class ProjectLinkImportParser +{ + private static final long MAX_FILE_SIZE = 5L * 1024L * 1024L; + + private static final int MAX_ROW_COUNT = 2000; + + private static final Charset GB18030 = Charset.forName("GB18030"); + + public List parse(MultipartFile file, String diskType) + { + validateFile(file); + String normalizedDiskType = normalizeDiskType(diskType); + String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT); + try + { + byte[] bytes = file.getBytes(); + List sourceRows; + if ("csv".equals(extension)) + { + sourceRows = readCsv(bytes); + } + else if ("xls".equals(extension) || "xlsx".equals(extension)) + { + sourceRows = readExcel(bytes); + } + else + { + throw new ServiceException("仅支持 csv、xls、xlsx 格式文件"); + } + if (sourceRows.size() > MAX_ROW_COUNT) + { + throw new ServiceException("单次最多导入 " + MAX_ROW_COUNT + " 条数据"); + } + validateHeaders(sourceRows, normalizedDiskType); + return convertRows(sourceRows, normalizedDiskType); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new ServiceException("文件解析失败,请确认文件格式与网盘类型是否正确"); + } + } + + private void validateFile(MultipartFile file) + { + if (file == null || file.isEmpty()) + { + throw new ServiceException("请选择需要导入的文件"); + } + if (file.getSize() > MAX_FILE_SIZE) + { + throw new ServiceException("导入文件不能超过 5MB"); + } + if (StringUtils.isBlank(file.getOriginalFilename())) + { + throw new ServiceException("无法识别导入文件名"); + } + } + + private String normalizeDiskType(String diskType) + { + String value = StringUtils.trimToEmpty(diskType).toUpperCase(Locale.ROOT); + if (!NetDiskConstants.DISK_QUARK.equals(value) && !NetDiskConstants.DISK_BAIDU.equals(value)) + { + throw new ServiceException("不支持的网盘类型"); + } + return value; + } + + private List readCsv(byte[] bytes) + { + String csvText = decodeCsv(bytes); + CsvReadConfig config = CsvReadConfig.defaultConfig() + .setContainsHeader(true) + .setSkipEmptyRows(true) + .setErrorOnDifferentFieldCount(true); + CsvData csvData = CsvUtil.getReader(new StringReader(csvText), config).read(); + List rows = new ArrayList<>(); + for (CsvRow csvRow : csvData.getRows()) + { + rows.add(new SourceRow((int) csvRow.getOriginalLineNumber() + 1, + normalizeFieldMap(csvRow.getFieldMap()))); + } + if (rows.isEmpty()) + { + rows.add(new SourceRow(1, headerOnlyMap(csvData.getHeader()))); + } + return rows; + } + + private List readExcel(byte[] bytes) + { + List rows = new ArrayList<>(); + try (ExcelReader reader = ExcelUtil.getReader(new ByteArrayInputStream(bytes))) + { + List> excelRows = reader.readAll(); + int rowNumber = 2; + for (Map excelRow : excelRows) + { + Map fields = new LinkedHashMap<>(); + for (Map.Entry entry : excelRow.entrySet()) + { + fields.put(normalizeHeader(entry.getKey()), objectToString(entry.getValue())); + } + rows.add(new SourceRow(rowNumber++, fields)); + } + if (rows.isEmpty()) + { + List headers = reader.readRow(0); + Map headerMap = new LinkedHashMap<>(); + for (Object header : headers) + { + headerMap.put(normalizeHeader(objectToString(header)), ""); + } + rows.add(new SourceRow(1, headerMap)); + } + } + return rows; + } + + private String decodeCsv(byte[] bytes) + { + try + { + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + CharBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes)); + return decoded.toString(); + } + catch (CharacterCodingException ignored) + { + return new String(bytes, GB18030); + } + } + + private Map normalizeFieldMap(Map source) + { + Map fields = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) + { + fields.put(normalizeHeader(entry.getKey()), entry.getValue()); + } + return fields; + } + + private Map headerOnlyMap(List headers) + { + Map fields = new LinkedHashMap<>(); + if (headers != null) + { + for (String header : headers) + { + fields.put(normalizeHeader(header), ""); + } + } + return fields; + } + + private void validateHeaders(List rows, String diskType) + { + Map fields = rows.get(0).fields; + List requiredHeaders = new ArrayList<>(); + if (NetDiskConstants.DISK_QUARK.equals(diskType)) + { + requiredHeaders.add("创建分享状态"); + requiredHeaders.add("分享名"); + requiredHeaders.add("分享地址"); + } + else + { + requiredHeaders.add("文件名"); + requiredHeaders.add("链接"); + requiredHeaders.add("分享状态"); + } + for (String header : requiredHeaders) + { + if (!fields.containsKey(header)) + { + throw new ServiceException("文件缺少必要列:" + header); + } + } + } + + private List convertRows(List sourceRows, String diskType) + { + List rows = new ArrayList<>(); + for (SourceRow sourceRow : sourceRows) + { + if (sourceRow.rowNumber == 1 && allValuesBlank(sourceRow.fields)) + { + continue; + } + ProjectLinkImportRow row = new ProjectLinkImportRow(); + row.setRowNumber(sourceRow.rowNumber); + if (NetDiskConstants.DISK_QUARK.equals(diskType)) + { + row.setProjectName(sourceRow.fields.get("分享名")); + row.setShareAddress(sourceRow.fields.get("分享地址")); + row.setExtractCode(sourceRow.fields.get("提取码")); + row.setShareStatus(sourceRow.fields.get("创建分享状态")); + } + else + { + row.setProjectName(sourceRow.fields.get("文件名")); + row.setShareAddress(sourceRow.fields.get("链接")); + row.setExtractCode(sourceRow.fields.get("提取码")); + row.setShareStatus(sourceRow.fields.get("分享状态")); + } + rows.add(row); + } + return rows; + } + + private boolean allValuesBlank(Map fields) + { + for (String value : fields.values()) + { + if (StringUtils.isNotBlank(value)) + { + return false; + } + } + return true; + } + + private String normalizeHeader(String header) + { + return StringUtils.trimToEmpty(header).replace("\uFEFF", ""); + } + + private String objectToString(Object value) + { + return value == null ? "" : String.valueOf(value); + } + + private static class SourceRow + { + private final int rowNumber; + + private final Map fields; + + private SourceRow(int rowNumber, Map fields) + { + this.rowNumber = rowNumber; + this.fields = fields; + } + } +} diff --git a/ruoyi-office/src/main/resources/mapper/office/TtCodeMapper.xml b/ruoyi-office/src/main/resources/mapper/office/TtCodeMapper.xml index 91410ea..e24e8b1 100644 --- a/ruoyi-office/src/main/resources/mapper/office/TtCodeMapper.xml +++ b/ruoyi-office/src/main/resources/mapper/office/TtCodeMapper.xml @@ -10,6 +10,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + @@ -19,7 +22,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select code_id, code_name, code_desc, code_environment, code_technology, code_source, payment_type, disk_link, publish_flag, picture_file, video_file from tt_code + select code_id, code_name, code_desc, code_environment, code_technology, + frontend_technology, backend_technology, database_technology, + code_source, payment_type, disk_link, publish_flag, picture_file, video_file + from tt_code + SELECT + (SELECT COUNT(1) + FROM app_resource + WHERE del_flag = 0) AS resource_total, + (SELECT COUNT(1) + FROM app_resource + WHERE del_flag = 0 + AND create_time >= CURDATE() + AND create_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)) AS resource_today, + (SELECT COUNT(1) + FROM sys_user + WHERE del_flag = '0') AS user_total, + (SELECT COUNT(1) + FROM sys_user + WHERE del_flag = '0' + AND create_time >= CURDATE() + AND create_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)) AS user_today, + ((SELECT COUNT(1) + FROM app_pay_order + WHERE status = 1) + + + (SELECT COUNT(1) + FROM app_virtual_order + WHERE status = 1)) AS order_total, + ((SELECT COUNT(1) + FROM app_pay_order + WHERE status = 1 + AND pay_time >= CURDATE() + AND pay_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)) + + + (SELECT COUNT(1) + FROM app_virtual_order + WHERE status = 1 + AND pay_time >= CURDATE() + AND pay_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY))) AS order_today, + CAST( + COALESCE((SELECT SUM(amount) + FROM app_pay_order + WHERE status = 1), 0) + + + COALESCE((SELECT SUM(price_fen) + FROM app_virtual_order + WHERE status = 1), 0) / 100 + AS DECIMAL(20, 2) + ) AS amount_total, + CAST( + COALESCE((SELECT SUM(amount) + FROM app_pay_order + WHERE status = 1 + AND pay_time >= CURDATE() + AND pay_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)), 0) + + + COALESCE((SELECT SUM(price_fen) + FROM app_virtual_order + WHERE status = 1 + AND pay_time >= CURDATE() + AND pay_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)), 0) / 100 + AS DECIMAL(20, 2) + ) AS amount_today + + diff --git a/ruoyi-system/src/main/resources/mapper/app/AppIntegralRecordMapper.xml b/ruoyi-system/src/main/resources/mapper/app/AppIntegralRecordMapper.xml index 91ea05a..08055f2 100644 --- a/ruoyi-system/src/main/resources/mapper/app/AppIntegralRecordMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/app/AppIntegralRecordMapper.xml @@ -36,6 +36,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ORDER BY a.integral_time desc + + - \ No newline at end of file + diff --git a/ruoyi-system/src/main/resources/mapper/app/AppResourceMapper.xml b/ruoyi-system/src/main/resources/mapper/app/AppResourceMapper.xml index 130e1a1..ff1440e 100644 --- a/ruoyi-system/src/main/resources/mapper/app/AppResourceMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/app/AppResourceMapper.xml @@ -33,6 +33,10 @@ + + + + @@ -65,10 +69,36 @@ select a.id, a.resource_title, a.show_img, a.`explain`,a.`keyword`, a.resource_type, a.is_show, a.is_ad, a.ad_number, a.price_fen, (select vp.product_id from app_virtual_product vp where vp.price_fen = a.price_fen and vp.status = 1 limit 1) as virtual_product_id, a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark, - b.id as sub_id, b.list_name as sub_list_name, b.list_url as sub_list_url, b.password as sub_password, b.app_resource_id as sub_app_resource_id + b.id as sub_id, b.list_name as sub_list_name, b.list_url as sub_list_url, + b.password as sub_password, b.price_fen as sub_price_fen, + b.status as sub_status, b.sort_order as sub_sort_order, + 0 as sub_purchased, b.app_resource_id as sub_app_resource_id from app_resource a left join app_resource_list b on b.app_resource_id = a.id where a.id = #{id} + order by b.sort_order asc, b.id asc + + + + + @@ -154,42 +184,129 @@ - insert into app_resource_list( id, list_name, list_url, password, app_resource_id) values + insert into app_resource_list + (id, list_name, list_url, password, price_fen, status, sort_order, app_resource_id) values - ( #{item.id}, #{item.listName}, #{item.listUrl}, #{item.password}, #{item.appResourceId}) + (#{item.id}, #{item.listName}, #{item.listUrl}, #{item.password}, + #{item.priceFen}, #{item.status}, #{item.sortOrder}, #{item.appResourceId}) + + + + diff --git a/ruoyi-system/src/main/resources/mapper/app/AppVirtualOrderMapper.xml b/ruoyi-system/src/main/resources/mapper/app/AppVirtualOrderMapper.xml index 734cebb..179d099 100644 --- a/ruoyi-system/src/main/resources/mapper/app/AppVirtualOrderMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/app/AppVirtualOrderMapper.xml @@ -11,6 +11,8 @@ + + @@ -24,31 +26,77 @@ + + + + + + + + + + + + + select vo.id, vo.order_no, vo.user_id, u.user_name, u.nick_name, - vo.resource_id, r.resource_title, vo.product_id, vo.price_fen, + vo.resource_id, r.resource_title, vo.resource_list_id, + coalesce(vo.spec_name_snapshot, rl.list_name, '旧版整项资源') as spec_name, + vo.product_id, vo.price_fen, vo.open_id, vo.status, vo.wx_order_no, vo.transaction_id, vo.create_time, vo.pay_time, vo.provide_time, vo.refund_time, vo.last_query_time from app_virtual_order vo left join sys_user u on u.user_id = vo.user_id left join app_resource r on r.id = vo.resource_id + left join app_resource_list rl on rl.id = vo.resource_list_id + + insert into app_virtual_order - (order_no, user_id, resource_id, product_id, price_fen, open_id, status, create_time) + (order_no, user_id, resource_id, resource_list_id, spec_name_snapshot, + product_id, price_fen, open_id, status, create_time) values - (#{orderNo}, #{userId}, #{resourceId}, #{productId}, #{priceFen}, #{openId}, #{status}, #{createTime}) + (#{orderNo}, #{userId}, #{resourceId}, #{resourceListId}, #{specName}, + #{productId}, #{priceFen}, #{openId}, #{status}, #{createTime}) + + + +