feat(merchant): 商户应用事件通知配置(entity/service/controller/菜单SQL/i18n)

This commit is contained in:
DaxPay Dev
2026-07-02 04:25:31 +08:00
parent 4bb5efdc71
commit 0ac65d7e6f
12 changed files with 291 additions and 0 deletions

View File

@@ -156,6 +156,8 @@ INSERT INTO "public"."iam_perm_menu" VALUES (90302, 9, 'device:printer', 'admin'
INSERT INTO "public"."iam_perm_menu" VALUES (40105, 405, 'payment:config:product_config', 'admin', 'ProductConfig', '支付产品管理', 'Payment Product Management', 'menu.payment.config.productConfig', 'lucide:layout-grid', 'f', 'f', '/payment/config/product/ProductConfig', '/payment/config/product', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-13 00:00:00+00', '2026-06-27 13:01:24.317032+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:provider_manage', 'admin', 'PayProviderManage', '支付渠道', 'Payment Provider', 'menu.payment.platform.provider.manage', 'lucide:wallet', 'f', 'f', '/payment/masterdata/provider/PayProviderList', '/payment/platform/pay-provider', NULL, 0.5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-17 10:00:31.640482+00', '2026-06-27 13:01:24.318649+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_config', 'admin', 'MchAppNotifyConfig', '通知配置', 'Notify Config', 'menu.payment.merchant.app.notifyConfig', NULL, 't', 'f', '/payment/merchant/app/MchAppNotifyConfig', '/payment/merchant/app/notify-config', NULL, 11, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-02 16:00:00+00', '2026-07-02 16:00:00+00');
-- ----------------------------
-- Primary Key structure for table iam_perm_menu
-- ----------------------------

View File

@@ -0,0 +1,2 @@
-- 商户应用事件通知配置: 菜单(权限码 merchant:notify_config)
INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_config', 'admin', 'MchAppNotifyConfig', '通知配置', 'Notify Config', 'menu.payment.merchant.app.notifyConfig', NULL, 't', 'f', '/payment/merchant/app/MchAppNotifyConfig', '/payment/merchant/app/notify-config', NULL, 11, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-02 16:00:00+00', '2026-07-02 16:00:00+00');

View File

@@ -0,0 +1,43 @@
package cn.daxpay.open.payment.admin.controller.merchant.config;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam;
import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult;
import cn.daxpay.open.payment.merchant.service.config.MchAppNotifyConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/// # 商户应用事件通知配置管理控制器
///
@PermCode(menuCode = "merchant:notify_config")
@Validated
@Tag(name = "商户应用事件通知配置管理")
@RestController
@RequestMapping("/admin/merchant/app-notify-config")
@RequiredArgsConstructor
public class MchAppNotifyConfigAdminController {
private final MchAppNotifyConfigService notifyConfigService;
@PermCode(code = "view", nameCn = "商户查看", nameEn = "Merchant View")
@Operation(summary = "根据应用ID查询通知配置")
@GetMapping("/get-by-app-id")
public Result<MchAppNotifyConfigResult> findByAppId(
@NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
return Res.ok(notifyConfigService.findByAppId(appId));
}
@PermCode(code = "notify_config_update", nameCn = "通知配置更新", nameEn = "Notify Config Update")
@Operation(summary = "保存或更新通知配置")
@PostMapping("/save-or-update")
public Result<Void> saveOrUpdate(@RequestBody @Validated MchAppNotifyConfigParam param) {
notifyConfigService.saveOrUpdate(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,24 @@
package cn.daxpay.open.payment.merchant.convert.config;
import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig;
import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam;
import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 商户应用事件通知配置转换
///
@Mapper
public interface MchAppNotifyConfigConvert {
MchAppNotifyConfigConvert CONVERT = Mappers.getMapper(MchAppNotifyConfigConvert.class);
MchAppNotifyConfig toEntity(MchAppNotifyConfigParam param);
MchAppNotifyConfigResult toResult(MchAppNotifyConfig entity);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(MchAppNotifyConfigParam param, @MappingTarget MchAppNotifyConfig entity);
}

View File

@@ -0,0 +1,18 @@
package cn.daxpay.open.payment.merchant.dao.config;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/// # 商户应用事件通知配置管理
///
@Repository
public class MchAppNotifyConfigManager extends BaseManager<MchAppNotifyConfigMapper, MchAppNotifyConfig> {
/// 根据应用ID查询
public Optional<MchAppNotifyConfig> findByAppId(String appId) {
return findByField(MchAppNotifyConfig::getAppId, appId);
}
}

View File

@@ -0,0 +1,11 @@
package cn.daxpay.open.payment.merchant.dao.config;
import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 商户应用事件通知配置
///
@Mapper
public interface MchAppNotifyConfigMapper extends MPJBaseMapper<MchAppNotifyConfig> {
}

View File

@@ -0,0 +1,44 @@
package cn.daxpay.open.payment.merchant.entity.config;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
import cn.daxpay.open.payment.merchant.convert.config.MchAppNotifyConfigConvert;
import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 商户应用事件通知配置
///
/// 应用级通用事件通知配置, 与支付订单级回调并行, 不局限于支付领域
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("mch_app_notify_config")
public class MchAppNotifyConfig extends MchBaseEntity implements ToResult<MchAppNotifyConfigResult> {
/// 应用ID
private String appId;
/// 回调地址 (https)
private String notifyUrl;
/// 通知方式 (http-HTTP异步回调)
private String notifyWay = "http";
/// 订阅事件类型 (逗号分隔, TradeTypeEnum的code: pay/cashouts/settle)
private String subscribedEvents;
/// 启用状态
private Boolean status = false;
/// 备注
private String remark;
/// 转换
@Override
public MchAppNotifyConfigResult toResult() {
return MchAppNotifyConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.payment.merchant.param.config;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 商户应用事件通知配置参数
///
@Data
@Accessors(chain = true)
@Schema(title = "商户应用事件通知配置参数")
public class MchAppNotifyConfigParam {
/// 应用ID
@Schema(description = "应用ID")
@NotBlank(message = "{validation.field.appId.notBlank}")
private String appId;
/// 回调地址
@Schema(description = "回调地址(https)")
@Size(max = 255, message = "{validation.field.notifyUrl.size}")
private String notifyUrl;
/// 通知方式
@Schema(description = "通知方式(http-HTTP异步回调)")
private String notifyWay;
/// 订阅事件类型
@Schema(description = "订阅事件类型(逗号分隔,TradeTypeEnum的code)")
private String subscribedEvents;
/// 启用状态
@Schema(description = "启用状态")
private Boolean status;
/// 备注
@Schema(description = "备注")
@Size(max = 255, message = "{validation.field.remark.size}")
private String remark;
}

View File

@@ -0,0 +1,44 @@
package cn.daxpay.open.payment.merchant.result.config;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 商户应用事件通知配置结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "商户应用事件通知配置结果")
public class MchAppNotifyConfigResult extends BaseResult {
/// 商户号
@Schema(description = "商户号")
private String mchNo;
/// 应用ID
@Schema(description = "应用ID")
private String appId;
/// 回调地址
@Schema(description = "回调地址")
private String notifyUrl;
/// 通知方式
@Schema(description = "通知方式")
private String notifyWay;
/// 订阅事件类型
@Schema(description = "订阅事件类型")
private String subscribedEvents;
/// 启用状态
@Schema(description = "启用状态")
private Boolean status;
/// 备注
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,55 @@
package cn.daxpay.open.payment.merchant.service.config;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import cn.daxpay.open.payment.merchant.convert.config.MchAppNotifyConfigConvert;
import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager;
import cn.daxpay.open.payment.merchant.dao.config.MchAppNotifyConfigManager;
import cn.daxpay.open.payment.merchant.entity.appinfo.MchAppInfo;
import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig;
import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam;
import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 商户应用事件通知配置服务
///
/// 应用级通用事件通知配置, 与支付订单级回调并行, 当前版本仅维护配置数据,
/// 发送链路(任务/重试/记录)后续阶段实现
@Slf4j
@Service
@RequiredArgsConstructor
public class MchAppNotifyConfigService {
private final MchAppNotifyConfigManager notifyConfigManager;
private final MchAppInfoManager mchAppInfoManager;
/// 根据应用ID查询通知配置, 无记录时返回默认空对象(不落库)
public MchAppNotifyConfigResult findByAppId(String appId) {
return notifyConfigManager.findByAppId(appId)
.map(MchAppNotifyConfig::toResult)
.orElseGet(() -> new MchAppNotifyConfigResult()
.setAppId(appId)
.setNotifyWay("http")
.setStatus(false));
}
/// 保存或更新(按应用ID upsert)
public void saveOrUpdate(MchAppNotifyConfigParam param) {
// 校验应用存在并获取商户号(通知配置冗余商户号, 便于按商户过滤与鉴权)
MchAppInfo mchApp = mchAppInfoManager.findByAppId(param.getAppId())
// 商户: 商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.payment.merchant.mchAppNotFound"));
var existing = notifyConfigManager.findByAppId(param.getAppId());
if (existing.isPresent()) {
MchAppNotifyConfig entity = existing.get();
MchAppNotifyConfigConvert.CONVERT.copy(param, entity);
notifyConfigManager.updateById(entity);
} else {
MchAppNotifyConfig entity = MchAppNotifyConfigConvert.CONVERT.toEntity(param);
entity.setMchNo(mchApp.getMchNo());
notifyConfigManager.save(entity);
}
}
}

View File

@@ -534,6 +534,9 @@
},
"preAuthToken": {
"notBlank": "Pre-auth token cannot be blank"
},
"remark": {
"size": "Remark cannot exceed 255 characters"
}
}
}

View File

@@ -537,6 +537,9 @@
},
"preAuthToken": {
"notBlank": "预认证令牌不能为空"
},
"remark": {
"size": "备注不可超过255位"
}
}
}