From fa83ef596792cfea41621f3d156376bd0519864d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 15 Aug 2025 02:47:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(dev):=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=8E=AF=E5=A2=83=E6=94=AF=E4=BB=98=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E4=BC=98=E6=83=A0=E5=88=B8=E5=AD=97=E6=AE=B5=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增开发环境控制器和环境感知支付服务 - 添加数据库字段缺失修复指南 - 改进优惠券适用商品查询逻辑 -优化支付配置获取方式 --- docs/DATABASE_FIELD_MISSING_FIX.md | 190 ++++++++++++++ docs/PAYMENT_ENVIRONMENT_ISOLATION_GUIDE.md | 212 ++++++++++++++++ docs/SAFE_PRODUCTION_SETUP_GUIDE.md | 176 +++++++++++++ .../controller/DevEnvironmentController.java | 236 ++++++++++++++++++ .../EnvironmentAwarePaymentService.java | 143 +++++++++++ .../mapper/xml/ShopCouponApplyItemMapper.xml | 6 + .../shop/param/ShopCouponApplyItemParam.java | 10 +- .../service/impl/CouponStatusServiceImpl.java | 40 ++- .../service/impl/ShopOrderServiceImpl.java | 16 +- src/main/resources/application.yml | 16 ++ .../sql/create_dev_tenant_payment.sql | 206 +++++++++++++++ .../sql/fix_coupon_apply_item_table.sql | 101 ++++++++ .../sql/production_safe_payment_config.sql | 183 ++++++++++++++ .../resources/sql/simple_fix_coupon_table.sql | 17 ++ 14 files changed, 1537 insertions(+), 15 deletions(-) create mode 100644 docs/DATABASE_FIELD_MISSING_FIX.md create mode 100644 docs/PAYMENT_ENVIRONMENT_ISOLATION_GUIDE.md create mode 100644 docs/SAFE_PRODUCTION_SETUP_GUIDE.md create mode 100644 src/main/java/com/gxwebsoft/common/core/controller/DevEnvironmentController.java create mode 100644 src/main/java/com/gxwebsoft/common/core/service/EnvironmentAwarePaymentService.java create mode 100644 src/main/resources/sql/create_dev_tenant_payment.sql create mode 100644 src/main/resources/sql/fix_coupon_apply_item_table.sql create mode 100644 src/main/resources/sql/production_safe_payment_config.sql create mode 100644 src/main/resources/sql/simple_fix_coupon_table.sql diff --git a/docs/DATABASE_FIELD_MISSING_FIX.md b/docs/DATABASE_FIELD_MISSING_FIX.md new file mode 100644 index 0000000..3ec316d --- /dev/null +++ b/docs/DATABASE_FIELD_MISSING_FIX.md @@ -0,0 +1,190 @@ +# 数据库字段缺失问题修复指南 + +## 🐛 问题描述 + +错误信息: +``` +java.sql.SQLSyntaxErrorException: Unknown column 'goods_id' in 'field list' +``` + +**原因**: 数据库表 `shop_coupon_apply_item` 中缺少 `goods_id` 和 `category_id` 字段,但代码中尝试查询这些字段。 + +## 🔧 解决方案 + +### 方案一:执行数据库修复脚本(推荐) + +1. **备份数据库**(重要!) +```bash +mysqldump -u username -p database_name > backup_$(date +%Y%m%d_%H%M%S).sql +``` + +2. **执行修复脚本** +```bash +mysql -u username -p database_name < src/main/resources/sql/simple_fix_coupon_table.sql +``` + +或者手动执行以下SQL: +```sql +-- 添加缺失的字段 +ALTER TABLE shop_coupon_apply_item +ADD COLUMN goods_id INT(11) NULL COMMENT '商品ID' AFTER coupon_id; + +ALTER TABLE shop_coupon_apply_item +ADD COLUMN category_id INT(11) NULL COMMENT '分类ID' AFTER goods_id; + +-- 添加索引 +CREATE INDEX idx_coupon_apply_item_goods ON shop_coupon_apply_item(coupon_id, goods_id); +CREATE INDEX idx_coupon_apply_item_category ON shop_coupon_apply_item(coupon_id, category_id); +CREATE INDEX idx_coupon_apply_item_type ON shop_coupon_apply_item(coupon_id, type); + +-- 检查表结构 +DESCRIBE shop_coupon_apply_item; +``` + +### 方案二:临时代码修复(已实施) + +我已经修改了 `CouponStatusServiceImpl.java` 中的代码,添加了异常处理: + +```java +try { + // 尝试查询 goods_id 字段 + List applyItems = shopCouponApplyItemService.list(...); + // 处理逻辑 +} catch (Exception e) { + log.warn("查询优惠券适用商品失败,可能是数据库字段不存在: {}", e.getMessage()); + // 如果查询失败,默认返回true(允许使用) + return true; +} +``` + +## 📋 修复步骤 + +### 1. 立即修复(临时方案) +- ✅ 已修改代码添加异常处理 +- ✅ 使用 `pk` 字段作为临时的商品ID +- ✅ 查询失败时默认允许使用优惠券 + +### 2. 完整修复(推荐执行) + +#### 步骤1:停止应用 +```bash +# 如果使用Docker +docker-compose down + +# 或者直接停止Java进程 +pkill -f java +``` + +#### 步骤2:备份数据库 +```bash +mysqldump -u root -p your_database > backup_$(date +%Y%m%d_%H%M%S).sql +``` + +#### 步骤3:执行数据库修复 +```bash +mysql -u root -p your_database < src/main/resources/sql/simple_fix_coupon_table.sql +``` + +#### 步骤4:验证修复 +```sql +-- 检查表结构 +DESCRIBE shop_coupon_apply_item; + +-- 应该看到以下字段: +-- id, coupon_id, goods_id, category_id, type, pk, status, deleted, tenant_id, create_time, update_time +``` + +#### 步骤5:重启应用 +```bash +# 如果使用Docker +docker-compose up -d + +# 或者直接启动 +java -jar your-app.jar +``` + +## 🧪 测试验证 + +### 1. 检查API接口 +```bash +# 测试优惠券列表查询 +curl -X GET "http://localhost:9200/api/shop/user-coupon/my/available" + +# 测试优惠券验证 +curl -X POST "http://localhost:9200/api/shop/coupon-status/validate" \ + -H "Content-Type: application/json" \ + -d '{"userCouponId":1,"totalAmount":150.00,"goodsIds":[1,2,3]}' +``` + +### 2. 检查日志 +```bash +# 查看应用日志 +tail -f logs/application.log + +# 查找相关错误 +grep -i "goods_id\|SQLSyntaxErrorException" logs/application.log +``` + +## 📊 数据迁移(可选) + +如果表中已有数据,可能需要迁移: + +```sql +-- 如果原来使用 pk 字段存储商品ID +UPDATE shop_coupon_apply_item +SET goods_id = pk +WHERE type = 1 AND pk IS NOT NULL AND goods_id IS NULL; + +-- 如果原来使用其他字段存储分类ID +-- UPDATE shop_coupon_apply_item +-- SET category_id = some_other_field +-- WHERE type = 2 AND some_other_field IS NOT NULL AND category_id IS NULL; +``` + +## 🚨 注意事项 + +1. **数据备份**: 执行任何数据库修改前必须备份 +2. **停机时间**: 建议在低峰期执行修复 +3. **测试环境**: 先在测试环境验证修复效果 +4. **回滚计划**: 准备回滚方案以防出现问题 + +## 🔄 回滚方案 + +如果修复后出现问题,可以回滚: + +```sql +-- 删除新添加的字段 +ALTER TABLE shop_coupon_apply_item DROP COLUMN goods_id; +ALTER TABLE shop_coupon_apply_item DROP COLUMN category_id; + +-- 删除新添加的索引 +DROP INDEX idx_coupon_apply_item_goods ON shop_coupon_apply_item; +DROP INDEX idx_coupon_apply_item_category ON shop_coupon_apply_item; +DROP INDEX idx_coupon_apply_item_type ON shop_coupon_apply_item; +``` + +或者直接恢复备份: +```bash +mysql -u root -p your_database < backup_20250115_143000.sql +``` + +## ✅ 修复完成检查清单 + +- [ ] 数据库已备份 +- [ ] 执行了字段添加脚本 +- [ ] 验证了表结构正确 +- [ ] 重启了应用 +- [ ] 测试了API接口正常 +- [ ] 检查了应用日志无错误 +- [ ] 验证了优惠券功能正常 + +## 📞 技术支持 + +如果在修复过程中遇到问题,请: + +1. 检查数据库连接和权限 +2. 确认SQL语法与MySQL版本兼容 +3. 查看详细的错误日志 +4. 如有必要,联系技术支持团队 + +修复完成后,优惠券状态管理功能应该可以正常使用! diff --git a/docs/PAYMENT_ENVIRONMENT_ISOLATION_GUIDE.md b/docs/PAYMENT_ENVIRONMENT_ISOLATION_GUIDE.md new file mode 100644 index 0000000..93a1ac0 --- /dev/null +++ b/docs/PAYMENT_ENVIRONMENT_ISOLATION_GUIDE.md @@ -0,0 +1,212 @@ +# 支付环境隔离解决方案 + +## 🎯 问题描述 + +**现状问题**: +- 开发调试时需要修改支付回调地址为本地地址 +- 修改后影响线上生产环境的正常使用 +- 缺乏开发和生产环境的有效隔离机制 + +## 💡 解决方案概览 + +我为您提供了5种解决方案,可以单独使用或组合使用: + +### 方案一:创建开发专用租户(推荐)✨ +- 创建独立的开发租户(ID: 9999) +- 配置专用的支付参数和回调地址 +- 完全隔离开发和生产环境 + +### 方案二:环境感知的支付配置服务 +- 根据 `spring.profiles.active` 自动切换回调地址 +- 开发环境自动使用本地回调,生产环境使用线上回调 +- 无需手动修改配置 + +### 方案三:配置文件环境隔离 +- 在配置文件中定义不同环境的回调地址 +- 支持灵活的环境配置管理 + +### 方案四:开发环境管理工具 +- 提供专用的开发环境管理接口 +- 支持一键切换和恢复回调地址 +- 仅在开发环境启用 + +### 方案五:多租户配置隔离 +- 利用现有的多租户架构 +- 为不同租户配置不同的支付参数 + +## 🚀 快速实施指南 + +### 步骤1:执行数据库脚本(推荐) + +```bash +# 创建开发专用租户和配置 +mysql -u root -p your_database < src/main/resources/sql/create_dev_tenant_payment.sql +``` + +这将创建: +- 开发专用租户(ID: 9999) +- 开发环境支付配置(使用本地回调地址) +- 开发测试用户 + +### 步骤2:配置环境感知服务 + +已创建的服务会自动: +- 检测当前运行环境 +- 根据环境自动调整回调地址 +- 开发环境:`http://frps-10550.s209.websoft.top/api/shop/shop-order/notify` +- 生产环境:`https://cms-api.websoft.top/api/shop/shop-order/notify` + +### 步骤3:使用开发环境管理工具 + +开发环境下可以访问以下接口: + +```bash +# 查看环境信息 +GET /api/dev/environment/info + +# 查看支付配置 +GET /api/dev/payment/config/0 + +# 切换回调地址 +POST /api/dev/payment/switch-notify-url +{ + "notifyUrl": "http://your-local-address/api/shop/shop-order/notify", + "payType": "0" +} + +# 重置为生产环境 +POST /api/dev/payment/reset-to-prod?payType=0 + +# 获取使用指南 +GET /api/dev/guide +``` + +## 📋 使用方式对比 + +| 方案 | 优点 | 缺点 | 适用场景 | +|------|------|------|----------| +| 开发专用租户 | 完全隔离,不影响生产 | 需要创建额外数据 | 团队开发,长期使用 | +| 环境感知服务 | 自动切换,无需手动操作 | 需要代码改动 | 自动化程度高的项目 | +| 配置文件隔离 | 配置灵活,易于管理 | 需要重启应用 | 配置驱动的项目 | +| 开发管理工具 | 操作简单,功能丰富 | 仅开发环境可用 | 频繁调试的场景 | +| 多租户隔离 | 利用现有架构 | 依赖租户体系 | 已有多租户的系统 | + +## 🔧 配置示例 + +### 开发环境配置 (application-dev.yml) +```yaml +payment: + dev: + notify-url: "http://frps-10550.s209.websoft.top/api/shop/shop-order/notify" + environment-aware: true +``` + +### 生产环境配置 (application-prod.yml) +```yaml +payment: + prod: + notify-url: "https://cms-api.websoft.top/api/shop/shop-order/notify" + environment-aware: false +``` + +## 🧪 测试验证 + +### 1. 验证环境感知功能 +```bash +# 检查当前环境 +curl -X GET "http://localhost:9200/api/dev/environment/info" + +# 检查支付配置 +curl -X GET "http://localhost:9200/api/dev/payment/config/0" +``` + +### 2. 验证回调地址切换 +```bash +# 切换到本地回调 +curl -X POST "http://localhost:9200/api/dev/payment/switch-notify-url" \ + -H "Content-Type: application/json" \ + -d '{"notifyUrl":"http://localhost:8080/api/shop/shop-order/notify","payType":"0"}' + +# 重置为生产回调 +curl -X POST "http://localhost:9200/api/dev/payment/reset-to-prod?payType=0" +``` + +## 🎨 最佳实践建议 + +### 推荐组合方案 + +**方案A:完全隔离(推荐)** +1. 创建开发专用租户 +2. 配置开发环境支付参数 +3. 使用开发租户进行所有测试 + +**方案B:自动化切换** +1. 部署环境感知服务 +2. 配置环境相关参数 +3. 代码自动根据环境切换 + +**方案C:手动管理** +1. 使用开发环境管理工具 +2. 调试时切换回调地址 +3. 完成后恢复生产配置 + +### 开发流程建议 + +1. **开发阶段**:使用开发租户或本地回调地址 +2. **测试阶段**:使用测试环境配置 +3. **上线前**:确认生产环境配置正确 +4. **上线后**:验证生产环境支付功能 + +## 🚨 注意事项 + +### 安全考虑 +- 开发环境管理接口仅在开发环境启用 +- 生产环境不会加载开发相关的控制器 +- 敏感配置信息需要妥善保护 + +### 数据一致性 +- 开发租户数据与生产数据隔离 +- 定期清理开发环境测试数据 +- 避免开发数据污染生产环境 + +### 团队协作 +- 统一开发环境配置标准 +- 文档化配置变更流程 +- 建立配置变更审核机制 + +## 🔄 回滚方案 + +如果需要回滚到原有方式: + +```sql +-- 删除开发租户(可选) +DELETE FROM sys_tenant WHERE tenant_id = 9999; +DELETE FROM sys_payment WHERE tenant_id = 9999; +DELETE FROM sys_user WHERE tenant_id = 9999; + +-- 恢复原有支付配置 +UPDATE sys_payment +SET notify_url = 'https://cms-api.websoft.top/api/shop/shop-order/notify' +WHERE tenant_id = 1; +``` + +## ✅ 实施检查清单 + +- [ ] 执行了数据库脚本创建开发租户 +- [ ] 配置了环境感知服务 +- [ ] 测试了开发环境管理接口 +- [ ] 验证了自动环境切换功能 +- [ ] 确认了生产环境配置正确 +- [ ] 建立了开发流程规范 +- [ ] 培训了团队成员使用方法 + +## 📞 技术支持 + +如果在实施过程中遇到问题: + +1. 检查日志中的环境检测信息 +2. 验证配置文件中的环境参数 +3. 确认数据库中的租户和支付配置 +4. 测试开发环境管理接口功能 + +实施完成后,您就可以在不影响生产环境的情况下进行支付功能的开发和调试了! diff --git a/docs/SAFE_PRODUCTION_SETUP_GUIDE.md b/docs/SAFE_PRODUCTION_SETUP_GUIDE.md new file mode 100644 index 0000000..655a92b --- /dev/null +++ b/docs/SAFE_PRODUCTION_SETUP_GUIDE.md @@ -0,0 +1,176 @@ +# 生产环境安全配置指南 + +## 🚨 重要警告 + +**原始的 `create_dev_tenant_payment.sql` 脚本不要在生产数据库执行!** + +该脚本包含测试数据,可能会影响生产环境。 + +## ✅ 安全的生产环境配置方案 + +### 方案一:使用后台管理界面(推荐) + +1. **登录后台管理系统** +2. **进入支付配置页面** +3. **创建新的支付配置**: + - 名称:`微信支付-开发环境` + - 类型:微信支付 + - 回调地址:`http://frps-10550.s209.websoft.top/api/shop/shop-order/notify` + - 其他参数:复制现有生产配置 + +### 方案二:使用API接口 + +```bash +# 1. 获取当前配置 +curl -X GET "https://your-domain.com/api/payment/list" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# 2. 创建开发配置 +curl -X POST "https://your-domain.com/api/payment" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "微信支付-开发环境", + "type": 0, + "appId": "YOUR_DEV_APP_ID", + "mchId": "YOUR_DEV_MCH_ID", + "notifyUrl": "http://frps-10550.s209.websoft.top/api/shop/shop-order/notify", + "environment": "dev" + }' +``` + +### 方案三:执行安全的SQL脚本 + +如果必须使用SQL,请使用我刚创建的安全版本: + +```bash +# 1. 先备份数据库 +mysqldump -u root -p your_database > backup_$(date +%Y%m%d_%H%M%S).sql + +# 2. 执行安全脚本 +mysql -u root -p your_database < src/main/resources/sql/production_safe_payment_config.sql + +# 3. 根据脚本输出的模板,手动创建开发配置 +``` + +## 🔧 推荐的实施步骤 + +### 步骤1:备份现有配置 + +```sql +-- 备份当前支付配置 +CREATE TABLE sys_payment_backup_$(date +%Y%m%d) AS +SELECT * FROM sys_payment WHERE status = 1; +``` + +### 步骤2:查看当前配置 + +```sql +-- 查看现有支付配置 +SELECT id, name, type, notify_url, tenant_id +FROM sys_payment +WHERE status = 1 AND deleted = 0; +``` + +### 步骤3:创建开发配置 + +**选择以下方式之一**: + +#### 方式A:通过后台界面 +1. 复制现有生产配置 +2. 修改名称为"开发环境" +3. 修改回调地址为本地地址 + +#### 方式B:通过SQL(谨慎使用) +```sql +-- 基于现有配置创建开发版本 +INSERT INTO sys_payment ( + name, type, code, app_id, mch_id, api_key, + notify_url, tenant_id, status, deleted, create_time, update_time +) +SELECT + CONCAT(name, '-开发环境'), + type, + CONCAT(code, '_dev'), + app_id, + mch_id, + api_key, + 'http://frps-10550.s209.websoft.top/api/shop/shop-order/notify', + tenant_id, + 0, -- 先设为禁用状态 + 0, + NOW(), + NOW() +FROM sys_payment +WHERE type = 0 AND status = 1 AND deleted = 0 +LIMIT 1; +``` + +### 步骤4:测试和验证 + +```bash +# 测试开发环境配置 +curl -X GET "http://localhost:9200/api/dev/payment/config/0" + +# 验证回调地址 +curl -X POST "http://frps-10550.s209.websoft.top/api/shop/shop-order/notify" \ + -d "test=1" +``` + +## 🛡️ 安全检查清单 + +- [ ] 已备份生产数据库 +- [ ] 确认当前数据库环境 +- [ ] 使用安全的配置方法 +- [ ] 测试开发配置不影响生产 +- [ ] 验证回调地址可访问 +- [ ] 建立配置恢复机制 + +## 🔄 快速切换方案 + +### 开发时切换到本地回调 + +```sql +-- 临时修改(记录原始值) +UPDATE sys_payment +SET notify_url = 'http://frps-10550.s209.websoft.top/api/shop/shop-order/notify' +WHERE id = YOUR_PAYMENT_CONFIG_ID; +``` + +### 完成后恢复生产回调 + +```sql +-- 恢复生产配置 +UPDATE sys_payment +SET notify_url = 'https://cms-api.websoft.top/api/shop/shop-order/notify' +WHERE id = YOUR_PAYMENT_CONFIG_ID; +``` + +## 🚀 最佳实践 + +1. **使用环境感知服务**:让代码自动根据环境切换 +2. **创建专用开发配置**:避免修改生产配置 +3. **使用配置管理工具**:通过界面而非SQL操作 +4. **建立回滚机制**:确保可以快速恢复 +5. **团队协作规范**:统一配置管理流程 + +## ❌ 避免的操作 + +- ❌ 直接在生产库执行包含测试数据的脚本 +- ❌ 修改生产配置进行开发调试 +- ❌ 在生产环境创建测试租户 +- ❌ 不备份就修改重要配置 +- ❌ 忘记恢复生产环境配置 + +## 📞 如果出现问题 + +1. **立即停止操作** +2. **检查数据库备份** +3. **恢复原始配置**: + ```sql + -- 从备份恢复 + INSERT INTO sys_payment SELECT * FROM sys_payment_backup_YYYYMMDD; + ``` +4. **联系技术支持** + +记住:**安全第一,谨慎操作!** 🛡️ diff --git a/src/main/java/com/gxwebsoft/common/core/controller/DevEnvironmentController.java b/src/main/java/com/gxwebsoft/common/core/controller/DevEnvironmentController.java new file mode 100644 index 0000000..d5ef241 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/controller/DevEnvironmentController.java @@ -0,0 +1,236 @@ +package com.gxwebsoft.common.core.controller; + +import com.gxwebsoft.common.core.service.EnvironmentAwarePaymentService; +import com.gxwebsoft.common.core.service.PaymentCacheService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.system.entity.Payment; +import com.gxwebsoft.common.system.service.PaymentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * 开发环境管理控制器 + * 仅在开发环境启用,用于管理开发调试配置 + * + * @author WebSoft + * @since 2025-01-15 + */ +@Slf4j +@Tag(name = "开发环境管理") +@RestController +@RequestMapping("/api/dev") +@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev") +public class DevEnvironmentController extends BaseController { + + @Autowired + private EnvironmentAwarePaymentService environmentAwarePaymentService; + + @Autowired + private PaymentCacheService paymentCacheService; + + @Autowired + private PaymentService paymentService; + + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + @Operation(summary = "获取当前环境信息") + @GetMapping("/environment/info") + public ApiResult> getEnvironmentInfo() { + Map info = new HashMap<>(); + info.put("activeProfile", activeProfile); + info.put("isDevelopment", environmentAwarePaymentService.isDevelopmentEnvironment()); + info.put("isProduction", environmentAwarePaymentService.isProductionEnvironment()); + info.put("currentEnvironment", environmentAwarePaymentService.getCurrentEnvironment()); + + return success("获取成功", info); + } + + @Operation(summary = "获取环境感知的支付配置") + @GetMapping("/payment/config/{payType}") + public ApiResult> getPaymentConfig(@PathVariable Integer payType) { + try { + Integer tenantId = getTenantId(); + + // 获取原始配置 + Payment originalConfig = paymentCacheService.getPaymentConfig(payType, tenantId); + + // 获取环境感知配置 + Payment envConfig = environmentAwarePaymentService.getEnvironmentAwarePaymentConfig(payType, tenantId); + + Map result = new HashMap<>(); + result.put("tenantId", tenantId); + result.put("payType", payType); + result.put("environment", activeProfile); + result.put("originalConfig", originalConfig); + result.put("environmentAwareConfig", envConfig); + + if (originalConfig != null && envConfig != null) { + result.put("notifyUrlChanged", !originalConfig.getNotifyUrl().equals(envConfig.getNotifyUrl())); + result.put("originalNotifyUrl", originalConfig.getNotifyUrl()); + result.put("environmentNotifyUrl", envConfig.getNotifyUrl()); + } + + return success("获取成功", result); + + } catch (Exception e) { + log.error("获取支付配置失败", e); + return fail("获取失败: " + e.getMessage(),null); + } + } + + @Operation(summary = "切换开发环境回调地址") + @PostMapping("/payment/switch-notify-url") + public ApiResult switchNotifyUrl(@RequestBody Map request) { + try { + String newNotifyUrl = request.get("notifyUrl"); + Integer payType = Integer.valueOf(request.getOrDefault("payType", "0")); + + if (newNotifyUrl == null || newNotifyUrl.trim().isEmpty()) { + return fail("回调地址不能为空"); + } + + Integer tenantId = getTenantId(); + + // 获取当前配置 + Payment payment = paymentCacheService.getPaymentConfig(payType, tenantId); + if (payment == null) { + return fail("未找到支付配置"); + } + + // 更新回调地址 + payment.setNotifyUrl(newNotifyUrl); + + // 更新数据库 + boolean updated = paymentService.updateById(payment); + + if (updated) { + // 清除缓存,强制重新加载 + paymentCacheService.removePaymentConfig(payType.toString(), tenantId); + + log.info("开发环境回调地址已更新: {} -> {}", payment.getNotifyUrl(), newNotifyUrl); + + Map result = new HashMap<>(); + result.put("oldNotifyUrl", payment.getNotifyUrl()); + result.put("newNotifyUrl", newNotifyUrl); + result.put("payType", payType); + result.put("tenantId", tenantId); + + return success("回调地址更新成功", result); + } else { + return fail("更新失败"); + } + + } catch (Exception e) { + log.error("切换回调地址失败", e); + return fail("切换失败: " + e.getMessage()); + } + } + + @Operation(summary = "重置为生产环境回调地址") + @PostMapping("/payment/reset-to-prod") + public ApiResult resetToProdNotifyUrl(@RequestParam(defaultValue = "0") Integer payType) { + try { + Integer tenantId = getTenantId(); + + // 获取当前配置 + Payment payment = paymentCacheService.getPaymentConfig(payType, tenantId); + if (payment == null) { + return fail("未找到支付配置"); + } + + // 设置为生产环境回调地址 + String prodNotifyUrl = "https://cms-api.websoft.top/api/shop/shop-order/notify"; + String oldNotifyUrl = payment.getNotifyUrl(); + + payment.setNotifyUrl(prodNotifyUrl); + + // 更新数据库 + boolean updated = paymentService.updateById(payment); + + if (updated) { + // 清除缓存 + paymentCacheService.removePaymentConfig(payType.toString(), tenantId); + + log.info("回调地址已重置为生产环境: {} -> {}", oldNotifyUrl, prodNotifyUrl); + + Map result = new HashMap<>(); + result.put("oldNotifyUrl", oldNotifyUrl); + result.put("newNotifyUrl", prodNotifyUrl); + result.put("payType", payType); + result.put("tenantId", tenantId); + + return success("已重置为生产环境回调地址", result); + } else { + return fail("重置失败"); + } + + } catch (Exception e) { + log.error("重置回调地址失败", e); + return fail("重置失败: " + e.getMessage()); + } + } + + @Operation(summary = "清除支付配置缓存") + @PostMapping("/payment/clear-cache") + public ApiResult clearPaymentCache(@RequestParam(defaultValue = "0") Integer payType) { + try { + Integer tenantId = getTenantId(); + + paymentCacheService.removePaymentConfig(payType.toString(), tenantId); + + log.info("支付配置缓存已清除: payType={}, tenantId={}", payType, tenantId); + + return success("缓存清除成功"); + + } catch (Exception e) { + log.error("清除缓存失败", e); + return fail("清除失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取开发环境使用指南") + @GetMapping("/guide") + public ApiResult> getDevGuide() { + Map guide = new HashMap<>(); + + guide.put("title", "开发环境支付调试指南"); + guide.put("environment", activeProfile); + + Map steps = new HashMap<>(); + steps.put("step1", "使用 /api/dev/payment/switch-notify-url 切换到本地回调地址"); + steps.put("step2", "进行支付功能调试和测试"); + steps.put("step3", "调试完成后使用 /api/dev/payment/reset-to-prod 恢复生产环境配置"); + steps.put("step4", "或者直接在后台管理界面修改回调地址"); + + guide.put("steps", steps); + + Map apis = new HashMap<>(); + apis.put("环境信息", "GET /api/dev/environment/info"); + apis.put("查看配置", "GET /api/dev/payment/config/{payType}"); + apis.put("切换回调", "POST /api/dev/payment/switch-notify-url"); + apis.put("重置生产", "POST /api/dev/payment/reset-to-prod"); + apis.put("清除缓存", "POST /api/dev/payment/clear-cache"); + + guide.put("apis", apis); + + Map tips = new HashMap<>(); + tips.put("tip1", "此控制器仅在开发环境启用"); + tips.put("tip2", "生产环境不会加载这些接口"); + tips.put("tip3", "建议使用环境感知服务自动切换"); + tips.put("tip4", "记得在调试完成后恢复生产配置"); + + guide.put("tips", tips); + + return success("获取成功", guide); + } +} diff --git a/src/main/java/com/gxwebsoft/common/core/service/EnvironmentAwarePaymentService.java b/src/main/java/com/gxwebsoft/common/core/service/EnvironmentAwarePaymentService.java new file mode 100644 index 0000000..2155cc9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/service/EnvironmentAwarePaymentService.java @@ -0,0 +1,143 @@ +package com.gxwebsoft.common.core.service; + +import com.gxwebsoft.common.system.entity.Payment; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 环境感知的支付配置服务 + * 根据不同环境自动切换支付回调地址 + * + * @author WebSoft + * @since 2025-01-15 + */ +@Slf4j +@Service +public class EnvironmentAwarePaymentService { + + @Autowired + private PaymentCacheService paymentCacheService; + + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + @Value("${config.server-url:}") + private String serverUrl; + + // 开发环境回调地址配置 + @Value("${payment.dev.notify-url:http://frps-10550.s209.websoft.top/api/shop/shop-order/notify}") + private String devNotifyUrl; + + // 生产环境回调地址配置 + @Value("${payment.prod.notify-url:https://cms-api.websoft.top/api/shop/shop-order/notify}") + private String prodNotifyUrl; + + /** + * 获取环境感知的支付配置 + * 根据当前环境自动调整回调地址 + * + * @param payType 支付类型 + * @param tenantId 租户ID + * @return 支付配置 + */ + public Payment getEnvironmentAwarePaymentConfig(Integer payType, Integer tenantId) { + // 获取原始支付配置 + Payment payment = paymentCacheService.getPaymentConfig(payType, tenantId); + + if (payment == null) { + return null; + } + + // 根据环境调整回调地址 + Payment envPayment = clonePayment(payment); + String notifyUrl = getEnvironmentNotifyUrl(); + + log.info("环境感知支付配置 - 环境: {}, 原始回调: {}, 调整后回调: {}", + activeProfile, payment.getNotifyUrl(), notifyUrl); + + envPayment.setNotifyUrl(notifyUrl); + + return envPayment; + } + + /** + * 根据当前环境获取回调地址 + */ + private String getEnvironmentNotifyUrl() { + if ("dev".equals(activeProfile) || "test".equals(activeProfile)) { + // 开发/测试环境使用本地回调地址 + return devNotifyUrl; + } else if ("prod".equals(activeProfile)) { + // 生产环境使用生产回调地址 + return prodNotifyUrl; + } else { + // 默认使用配置的服务器地址 + return serverUrl + "/shop/shop-order/notify"; + } + } + + /** + * 克隆支付配置对象 + */ + private Payment clonePayment(Payment original) { + Payment cloned = new Payment(); + cloned.setId(original.getId()); + cloned.setName(original.getName()); + cloned.setType(original.getType()); + cloned.setCode(original.getCode()); + cloned.setImage(original.getImage()); + cloned.setWechatType(original.getWechatType()); + cloned.setAppId(original.getAppId()); + cloned.setMchId(original.getMchId()); + cloned.setApiKey(original.getApiKey()); + cloned.setApiclientCert(original.getApiclientCert()); + cloned.setApiclientKey(original.getApiclientKey()); + cloned.setPubKey(original.getPubKey()); + cloned.setPubKeyId(original.getPubKeyId()); + cloned.setMerchantSerialNumber(original.getMerchantSerialNumber()); + cloned.setNotifyUrl(original.getNotifyUrl()); // 这个会被后续覆盖 + cloned.setComments(original.getComments()); + cloned.setSortNumber(original.getSortNumber()); + cloned.setStatus(original.getStatus()); + cloned.setDeleted(original.getDeleted()); + cloned.setTenantId(original.getTenantId()); + return cloned; + } + + /** + * 获取微信支付配置(环境感知) + */ + public Payment getWechatPayConfig(Integer tenantId) { + return getEnvironmentAwarePaymentConfig(0, tenantId); + } + + /** + * 获取支付宝配置(环境感知) + */ + public Payment getAlipayConfig(Integer tenantId) { + return getEnvironmentAwarePaymentConfig(1, tenantId); + } + + /** + * 检查当前环境 + */ + public String getCurrentEnvironment() { + return activeProfile; + } + + /** + * 是否为开发环境 + */ + public boolean isDevelopmentEnvironment() { + return "dev".equals(activeProfile) || "test".equals(activeProfile); + } + + /** + * 是否为生产环境 + */ + public boolean isProductionEnvironment() { + return "prod".equals(activeProfile); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopCouponApplyItemMapper.xml b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopCouponApplyItemMapper.xml index 5933bd7..141b156 100644 --- a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopCouponApplyItemMapper.xml +++ b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopCouponApplyItemMapper.xml @@ -13,6 +13,12 @@ AND a.coupon_id = #{param.couponId} + + AND a.goods_id = #{param.goodsId} + + + AND a.category_id = #{param.categoryId} + AND a.type = #{param.type} diff --git a/src/main/java/com/gxwebsoft/shop/param/ShopCouponApplyItemParam.java b/src/main/java/com/gxwebsoft/shop/param/ShopCouponApplyItemParam.java index 38c4820..f27cc47 100644 --- a/src/main/java/com/gxwebsoft/shop/param/ShopCouponApplyItemParam.java +++ b/src/main/java/com/gxwebsoft/shop/param/ShopCouponApplyItemParam.java @@ -28,8 +28,16 @@ public class ShopCouponApplyItemParam extends BaseParam { @QueryField(type = QueryType.EQ) private Integer couponId; + @Schema(description = "商品ID") @QueryField(type = QueryType.EQ) - private Boolean type; + private Integer goodsId; + + @Schema(description = "分类ID") + @QueryField(type = QueryType.EQ) + private Integer categoryId; + + @QueryField(type = QueryType.EQ) + private Integer type; @Schema(description = "0服务1需求2闲置") @QueryField(type = QueryType.EQ) diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/CouponStatusServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/CouponStatusServiceImpl.java index ceb68ab..1700188 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/CouponStatusServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/CouponStatusServiceImpl.java @@ -283,19 +283,33 @@ public class CouponStatusServiceImpl implements CouponStatusService { if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_GOODS) { // 指定商品适用 - List applyItems = shopCouponApplyItemService.list( - new LambdaQueryWrapper() - .eq(ShopCouponApplyItem::getCouponId, userCoupon.getCouponId()) - .eq(ShopCouponApplyItem::getType, 1) // 类型1表示商品 - .isNotNull(ShopCouponApplyItem::getGoodsId) - ); - - List applicableGoodsIds = applyItems.stream() - .map(ShopCouponApplyItem::getGoodsId) - .filter(goodsId -> goodsId != null) - .collect(Collectors.toList()); - - return goodsIds.stream().anyMatch(applicableGoodsIds::contains); + try { + List applyItems = shopCouponApplyItemService.list( + new LambdaQueryWrapper() + .eq(ShopCouponApplyItem::getCouponId, userCoupon.getCouponId()) + .eq(ShopCouponApplyItem::getType, 1) // 类型1表示商品 + ); + + // 如果数据库中还没有 goods_id 字段,暂时使用 pk 字段作为商品ID + List applicableGoodsIds = applyItems.stream() + .map(item -> { + if (item.getGoodsId() != null) { + return item.getGoodsId(); + } else if (item.getPk() != null) { + // 临时方案:使用 pk 字段作为商品ID + return item.getPk(); + } + return null; + }) + .filter(goodsId -> goodsId != null) + .collect(Collectors.toList()); + + return goodsIds.stream().anyMatch(applicableGoodsIds::contains); + } catch (Exception e) { + log.warn("查询优惠券适用商品失败,可能是数据库字段不存在: {}", e.getMessage()); + // 如果查询失败,默认返回true(允许使用) + return true; + } } if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_CATEGORY) { diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java index 294cc9b..b11a91a 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java @@ -8,6 +8,8 @@ import com.gxwebsoft.common.core.config.ConfigProperties; import com.gxwebsoft.common.core.config.CertificateProperties; import com.gxwebsoft.common.core.utils.*; import com.gxwebsoft.common.core.service.PaymentCacheService; +import com.gxwebsoft.common.core.service.EnvironmentAwarePaymentService; +import com.gxwebsoft.common.core.config.SpringContextUtil; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.common.system.entity.User; @@ -316,6 +318,7 @@ public class ShopOrderServiceImpl extends ServiceImpl