feat(device): 码牌支持 H5/小程序落地类型并精简 MapStruct 映射

新增 QrCodeProgramTypeEnum,扫码链接按类型分流 /h 与 /m;批量创建写入 programType(创建后不可改),并补齐校验与多语 i18n。Convert 改为单源同名映射,资金字段由 Service 补充;清空已落地的 update-tables.sql。
This commit is contained in:
DaxPay Dev
2026-07-15 16:48:13 +08:00
parent 55ca3173fc
commit 4e780b6860
45 changed files with 195 additions and 288 deletions

View File

@@ -1,220 +0,0 @@
-- 付款码列bar_code → auth_code与 unipay 入参 authCode 对齐)
ALTER TABLE pay_normal_order RENAME COLUMN bar_code TO auth_code;
COMMENT ON COLUMN pay_normal_order.auth_code IS '付款码(被扫支付,审计保留)';
-- 订单来源: 容器权威 + pay_trade 冗余(方案 C)
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS source varchar(32);
COMMENT ON COLUMN pay_normal_order.source IS '订单来源(业务入口权威, TradeSourceEnum)';
ALTER TABLE pay_gateway_order ADD COLUMN IF NOT EXISTS source varchar(32);
COMMENT ON COLUMN pay_gateway_order.source IS '订单来源(业务入口权威, TradeSourceEnum; 预下单写入)';
-- 历史数据: 从 pay_trade 回填容器(仅空值)
UPDATE pay_normal_order o
SET source = t.source
FROM pay_trade t
WHERE t.container_id = o.id
AND t.trade_type = 'normal'
AND t.deleted = false
AND o.source IS NULL
AND t.source IS NOT NULL;
UPDATE pay_gateway_order o
SET source = t.source
FROM pay_trade t
WHERE t.container_id = o.id
AND t.trade_type = 'gateway'
AND t.deleted = false
AND o.source IS NULL
AND t.source IS NOT NULL;
-- 网关预下单尚未建 trade 的: 按 gateway_type 派生
UPDATE pay_gateway_order
SET source = CASE gateway_type
WHEN 'cashier' THEN 'cashier'
ELSE 'aggress_pay'
END
WHERE source IS NULL
AND gateway_type IS NOT NULL;
-- ============================================================
-- 易支付协议插件表
-- ============================================================
-- 易支付凭证(应用级)
CREATE TABLE IF NOT EXISTS pay_easy_pay_credential (
id int8 NOT NULL,
pid int4 NOT NULL,
app_id varchar(32) NOT NULL,
enable bool NOT NULL DEFAULT false,
enable_v1 bool NOT NULL DEFAULT false,
enable_v2 bool NOT NULL DEFAULT true,
md5_key varchar(128),
use_system_key bool NOT NULL DEFAULT true,
public_key text,
mch_no varchar(32) NOT NULL,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 NOT NULL DEFAULT 0,
deleted bool NOT NULL DEFAULT false,
CONSTRAINT pk_pay_easy_pay_credential PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_easy_pay_credential_app_id ON pay_easy_pay_credential (app_id) WHERE deleted = false;
CREATE UNIQUE INDEX IF NOT EXISTS uk_easy_pay_credential_pid ON pay_easy_pay_credential (pid) WHERE deleted = false;
COMMENT ON TABLE pay_easy_pay_credential IS '易支付凭证配置表(应用级)';
COMMENT ON COLUMN pay_easy_pay_credential.id IS '主键';
COMMENT ON COLUMN pay_easy_pay_credential.pid IS '易支付商户号';
COMMENT ON COLUMN pay_easy_pay_credential.app_id IS '应用号(支付出口)';
COMMENT ON COLUMN pay_easy_pay_credential.enable IS '是否启用';
COMMENT ON COLUMN pay_easy_pay_credential.enable_v1 IS '是否开启V1接口';
COMMENT ON COLUMN pay_easy_pay_credential.enable_v2 IS '是否开启V2接口';
COMMENT ON COLUMN pay_easy_pay_credential.md5_key IS 'V1 MD5密钥';
COMMENT ON COLUMN pay_easy_pay_credential.use_system_key IS 'V2是否使用系统公私钥';
COMMENT ON COLUMN pay_easy_pay_credential.public_key IS '商户RSA公钥';
COMMENT ON COLUMN pay_easy_pay_credential.mch_no IS '商户号';
COMMENT ON COLUMN pay_easy_pay_credential.creator IS '创建人';
COMMENT ON COLUMN pay_easy_pay_credential.create_time IS '创建时间';
COMMENT ON COLUMN pay_easy_pay_credential.last_modifier IS '最后修改人';
COMMENT ON COLUMN pay_easy_pay_credential.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN pay_easy_pay_credential.version IS '乐观锁版本';
COMMENT ON COLUMN pay_easy_pay_credential.deleted IS '逻辑删除';
-- 易支付场景配置
CREATE TABLE IF NOT EXISTS pay_easy_pay_config (
id int8 NOT NULL,
pid int4 NOT NULL,
app_id varchar(32) NOT NULL,
limit_pay varchar(64),
mch_no varchar(32) NOT NULL,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 NOT NULL DEFAULT 0,
deleted bool NOT NULL DEFAULT false,
CONSTRAINT pk_pay_easy_pay_config PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_easy_pay_config_app_id ON pay_easy_pay_config (app_id) WHERE deleted = false;
CREATE UNIQUE INDEX IF NOT EXISTS uk_easy_pay_config_pid ON pay_easy_pay_config (pid) WHERE deleted = false;
COMMENT ON TABLE pay_easy_pay_config IS '易支付场景配置表';
COMMENT ON COLUMN pay_easy_pay_config.id IS '主键';
COMMENT ON COLUMN pay_easy_pay_config.pid IS '易支付商户号';
COMMENT ON COLUMN pay_easy_pay_config.app_id IS '应用号';
COMMENT ON COLUMN pay_easy_pay_config.limit_pay IS '限制支付方式';
COMMENT ON COLUMN pay_easy_pay_config.mch_no IS '商户号';
COMMENT ON COLUMN pay_easy_pay_config.creator IS '创建人';
COMMENT ON COLUMN pay_easy_pay_config.create_time IS '创建时间';
COMMENT ON COLUMN pay_easy_pay_config.last_modifier IS '最后修改人';
COMMENT ON COLUMN pay_easy_pay_config.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN pay_easy_pay_config.version IS '乐观锁版本';
COMMENT ON COLUMN pay_easy_pay_config.deleted IS '逻辑删除';
-- 易支付协议订单
CREATE TABLE IF NOT EXISTS pay_easy_pay_order (
id int8 NOT NULL,
order_id int8,
pid int4 NOT NULL,
app_id varchar(32) NOT NULL,
trade_no varchar(64),
out_trade_no varchar(100) NOT NULL,
api_trade_no varchar(64),
type varchar(32),
status int4 NOT NULL DEFAULT 0,
add_time timestamptz(6),
end_time timestamptz(6),
name varchar(200),
money numeric(16, 2) NOT NULL,
refund_money numeric(16, 2) DEFAULT 0,
notify_url varchar(500),
return_url varchar(500),
param varchar(500),
buyer varchar(128),
client_ip varchar(64),
api_version varchar(8),
pc_call_type varchar(32),
pay_url varchar(1000),
pay_body text,
mch_no varchar(32) NOT NULL,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 NOT NULL DEFAULT 0,
deleted bool NOT NULL DEFAULT false,
CONSTRAINT pk_pay_easy_pay_order PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_easy_pay_order_app_out ON pay_easy_pay_order (app_id, out_trade_no) WHERE deleted = false;
CREATE INDEX IF NOT EXISTS idx_easy_pay_order_pid_out ON pay_easy_pay_order (pid, out_trade_no);
CREATE INDEX IF NOT EXISTS idx_easy_pay_order_trade_no ON pay_easy_pay_order (trade_no);
CREATE INDEX IF NOT EXISTS idx_easy_pay_order_order_id ON pay_easy_pay_order (order_id);
COMMENT ON TABLE pay_easy_pay_order IS '易支付协议订单表';
COMMENT ON COLUMN pay_easy_pay_order.id IS '主键(收银台路径参数)';
COMMENT ON COLUMN pay_easy_pay_order.order_id IS '关联内核容器ID';
COMMENT ON COLUMN pay_easy_pay_order.pid IS '易支付商户号';
COMMENT ON COLUMN pay_easy_pay_order.app_id IS '应用号';
COMMENT ON COLUMN pay_easy_pay_order.trade_no IS '平台业务单号容器orderNo';
COMMENT ON COLUMN pay_easy_pay_order.out_trade_no IS '商户订单号';
COMMENT ON COLUMN pay_easy_pay_order.api_trade_no IS '通道订单号';
COMMENT ON COLUMN pay_easy_pay_order.type IS '协议支付方式 alipay/wxpay/aggregate';
COMMENT ON COLUMN pay_easy_pay_order.status IS '协议状态 0待付 1成功';
COMMENT ON COLUMN pay_easy_pay_order.add_time IS '创建时间';
COMMENT ON COLUMN pay_easy_pay_order.end_time IS '完成时间';
COMMENT ON COLUMN pay_easy_pay_order.name IS '商品名称';
COMMENT ON COLUMN pay_easy_pay_order.money IS '订单金额(元)';
COMMENT ON COLUMN pay_easy_pay_order.refund_money IS '已退款金额(元)';
COMMENT ON COLUMN pay_easy_pay_order.notify_url IS '异步通知地址(本期仅落库)';
COMMENT ON COLUMN pay_easy_pay_order.return_url IS '同步跳转地址';
COMMENT ON COLUMN pay_easy_pay_order.param IS '业务扩展参数';
COMMENT ON COLUMN pay_easy_pay_order.buyer IS '支付用户标识';
COMMENT ON COLUMN pay_easy_pay_order.client_ip IS '客户端IP';
COMMENT ON COLUMN pay_easy_pay_order.api_version IS 'API版本 v1/v2';
COMMENT ON COLUMN pay_easy_pay_order.pc_call_type IS '支付调用方式';
COMMENT ON COLUMN pay_easy_pay_order.pay_url IS '支付链接';
COMMENT ON COLUMN pay_easy_pay_order.pay_body IS '支付参数体';
COMMENT ON COLUMN pay_easy_pay_order.mch_no IS '商户号';
COMMENT ON COLUMN pay_easy_pay_order.creator IS '创建人';
COMMENT ON COLUMN pay_easy_pay_order.create_time IS '创建时间';
COMMENT ON COLUMN pay_easy_pay_order.last_modifier IS '最后修改人';
COMMENT ON COLUMN pay_easy_pay_order.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN pay_easy_pay_order.version IS '乐观锁版本';
COMMENT ON COLUMN pay_easy_pay_order.deleted IS '逻辑删除';
-- ============================================================
-- 订单门店维度: store_no线下经营归属可空
-- ============================================================
-- 订单门店维度: store_no线下经营归属可空
-- ============================================================
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS store_no varchar(32);
COMMENT ON COLUMN pay_normal_order.store_no IS '门店号(线下经营归属, 可空; 对应 mch_store_info.store_no)';
ALTER TABLE pay_gateway_order ADD COLUMN IF NOT EXISTS store_no varchar(32);
COMMENT ON COLUMN pay_gateway_order.store_no IS '门店号(线下经营归属, 可空; 对应 mch_store_info.store_no)';
ALTER TABLE pay_refund_order ADD COLUMN IF NOT EXISTS store_no varchar(32);
COMMENT ON COLUMN pay_refund_order.store_no IS '门店号(继承自原支付容器, 可空)';
CREATE INDEX IF NOT EXISTS idx_normal_order_mch_store ON pay_normal_order (mch_no, store_no);
CREATE INDEX IF NOT EXISTS idx_gateway_order_mch_store ON pay_gateway_order (mch_no, store_no);
CREATE INDEX IF NOT EXISTS idx_refund_order_mch_store ON pay_refund_order (mch_no, store_no);
-- ============================================================
-- pay_trade 轻量组织冗余: channel_mch_no + store_no
-- ============================================================
ALTER TABLE pay_trade ADD COLUMN IF NOT EXISTS channel_mch_no varchar(64);
COMMENT ON COLUMN pay_trade.channel_mch_no IS '通道商户号(冗余自业务容器, 路由确定后写入)';
ALTER TABLE pay_trade ADD COLUMN IF NOT EXISTS store_no varchar(32);
COMMENT ON COLUMN pay_trade.store_no IS '门店号(冗余自业务容器, 可空; 权威在容器)';
CREATE INDEX IF NOT EXISTS idx_pay_trade_mch_channel ON pay_trade (mch_no, channel_mch_no);
CREATE INDEX IF NOT EXISTS idx_pay_trade_mch_store ON pay_trade (mch_no, store_no);

View File

@@ -33,6 +33,12 @@ public class DeviceQrCodeBatchParam {
@Size(max = 100, message = "{validation.field.qrCodeName.size}")
private String name;
/// 落地程序类型: h5 / mini_app, 整批统一, 创建后不可改
/// @see cn.daxpay.open.payment.device.enums.QrCodeProgramTypeEnum
@Schema(description = "落地程序类型(h5-H5码牌/mini_app-小程序码牌)")
@NotBlank(message = "{validation.field.programType.notBlank}")
private String programType;
/// 金额类型: random-自定义金额 / fixed-固定金额
/// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum
@Schema(description = "金额类型(random-自定义/fixed-固定)")

View File

@@ -2,6 +2,7 @@ package cn.daxpay.open.payment.admin.service.device;
import cn.daxpay.open.payment.common.context.MerchantContextLoader;
import cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum;
import cn.daxpay.open.payment.device.enums.QrCodeProgramTypeEnum;
import cn.daxpay.open.payment.device.enums.QrCodeStatusEnum;
import cn.daxpay.open.payment.device.qrcode.dao.DeviceQrCodeManager;
import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode;
@@ -38,8 +39,11 @@ import java.util.stream.IntStream;
@RequiredArgsConstructor
public class DeviceQrCodeAdminService {
/// H5 码牌支付页路径前缀(与 dax-pay-h5 RoutePath.CODE_PAY 一致)
private static final String CODE_PAY_PATH = "/code-pay/";
/// H5 码牌支付页路径前缀(与 dax-pay-h5 RoutePath.CODE_PAY 一致: /h/:code)
private static final String CODE_H5_PATH = "/h/";
/// 小程序码牌扫码 path 前缀(映射域名落地后置, 本期仅生成链接)
private static final String CODE_MINI_PATH = "/m/";
private final DeviceQrCodeManager deviceQrCodeManager;
private final MerchantContextLoader merchantContextLoader;
@@ -54,6 +58,8 @@ public class DeviceQrCodeAdminService {
// 码牌: 批次号已存在
throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.batchNoExists");
}
// 落地程序类型(创建后不可改)
QrCodeProgramTypeEnum programType = QrCodeProgramTypeEnum.findByCode(param.getProgramType());
// 金额类型校验
QrCodeAmountTypeEnum amountType = QrCodeAmountTypeEnum.findByCode(param.getAmountType());
validateFixedAmount(amountType, param.getFixedAmount());
@@ -68,6 +74,7 @@ public class DeviceQrCodeAdminService {
.setCode(param.getBatchNo() + String.format("%03d", i))
.setName(param.getName())
.setBatchNo(param.getBatchNo())
.setProgramType(programType.getCode())
.setAmountType(amountType.getCode())
.setFixedAmount(fixedAmount)
.setStatus(status)
@@ -152,11 +159,13 @@ public class DeviceQrCodeAdminService {
deviceQrCodeManager.updateById(entity);
}
/// 获取码牌 H5 扫码链接
/// 获取码牌扫码链接
///
/// 拼接规则: {paymentGatewayBaseUrl}/code-pay/{code}
/// 同一 paymentGatewayBaseUrl 下按 programType 分流 path:
/// - h5 → /h/{code}
/// - mini_app → /m/{code}
public String getCodeLink(String code) {
deviceQrCodeManager.findByCode(code)
DeviceQrCode qrCode = deviceQrCodeManager.findByCode(code)
// 码牌: 码牌不存在
.orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound"));
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
@@ -165,7 +174,11 @@ public class DeviceQrCodeAdminService {
// 支付网关前端地址未配置
throw new OperationFailException(CommonCode.FAIL_CODE, "error.common.gatewayUrlNotConfigured");
}
return gatewayBase + CODE_PAY_PATH + code;
// 按落地程序类型选 path; 历史空值按 H5
String path = QrCodeProgramTypeEnum.MINI_APP.getCode().equals(qrCode.getProgramType())
? CODE_MINI_PATH
: CODE_H5_PATH;
return gatewayBase + path + code;
}
/// 应用解析: 复用 [MerchantContextLoader#resolveApp], appId 空取商户默认应用, 返回解析后的 appId

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.payment.device.enums;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import cn.daxpay.open.platform.core.i18n.I18nSupport;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/// # 码牌落地程序类型
///
/// 扫码链接 path 按类型分流: H5 为 `/h/{code}`, 小程序为 `/m/{code}`。
/// 字典: qrcode_program_type
@Getter
@RequiredArgsConstructor
public enum QrCodeProgramTypeEnum implements I18nSupport {
/// H5 码牌(扫码进入支付网关 H5 页)
H5("h5"),
/// 小程序码牌(扫码进入小程序映射 path, 落地页后置)
MINI_APP("mini_app");
/// 编码
private final String code;
/// 翻译 key 前缀
@Override
public String getI18nPrefix() {
return "enum.qrcode_program_type";
}
/// 根据编码查找
public static QrCodeProgramTypeEnum findByCode(String code) {
return Arrays.stream(values())
.filter(e -> e.getCode().equals(code))
.findFirst()
// 通用: 未找到对应的码牌程序类型: {0}
.orElseThrow(() -> new DataNotExistException("error.device.qrcode.programTypeNotFound", code));
}
}

View File

@@ -4,6 +4,8 @@ import cn.daxpay.open.payment.device.qrcode.convert.DeviceQrCodeConvert;
import cn.daxpay.open.payment.device.qrcode.result.DeviceQrCodeResult;
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -11,7 +13,8 @@ import lombok.experimental.Accessors;
/// # 支付码牌
///
/// 记录码牌与商户/应用的绑定关系, 扫码后进入 H5 码牌支付页(/code-pay/:code)
/// 记录码牌与商户/应用的绑定关系。
/// 扫码链接按 [programType] 分流: H5 为 `/h/{code}`, 小程序为 `/m/{code}`。
/// 支持运营端批量创建空白码牌后划拨给商户: mchNo 初始为空, 绑定后写入。
@EqualsAndHashCode(callSuper = true)
@Data
@@ -34,6 +37,11 @@ public class DeviceQrCode extends MpBaseEntity implements ToResult<DeviceQrCodeR
/// 关联应用号(可空, 空白码为空; 绑定后空则使用商户默认应用)
private String appId;
/// 落地程序类型: h5 / mini_app, 创建写入后不可改
/// @see cn.daxpay.open.payment.device.enums.QrCodeProgramTypeEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String programType;
/// 金额类型: random-自定义金额 / fixed-固定金额
/// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum
private String amountType;

View File

@@ -31,6 +31,12 @@ public class DeviceQrCodeQuery {
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
private String mchNo;
/// 落地程序类型
/// @see cn.daxpay.open.payment.device.enums.QrCodeProgramTypeEnum
@Schema(description = "落地程序类型(h5/mini_app)")
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
private String programType;
/// 金额类型
/// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum
@Schema(description = "金额类型")

View File

@@ -35,6 +35,11 @@ public class DeviceQrCodeResult extends BaseResult {
@Schema(description = "关联应用号(空表示使用商户默认应用)")
private String appId;
/// 落地程序类型
/// @see cn.daxpay.open.payment.device.enums.QrCodeProgramTypeEnum
@Schema(description = "落地程序类型(h5/mini_app)")
private String programType;
/// 金额类型
/// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum
@Schema(description = "金额类型(random-自定义/fixed-固定)")

View File

@@ -3,17 +3,15 @@ package cn.daxpay.open.payment.route.convert.basic;
import cn.daxpay.open.payment.route.entity.basic.PayRouteBasicConfig;
import cn.daxpay.open.payment.route.result.basic.PayRouteBasicConfigResult;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/// # 支付通道路由基础模式配置转换
///
/// 同名映射channelMchants(可选通道商户列表)由 service 按商户开通情况装配
@Mapper
public interface PayRouteBasicConfigConvert {
PayRouteBasicConfigConvert CONVERT = Mappers.getMapper(PayRouteBasicConfigConvert.class);
/// channelMchants(可选通道商户列表)由 service 按商户开通情况装配,转换时忽略
@Mapping(target = "channelMchants", ignore = true)
PayRouteBasicConfigResult toResult(PayRouteBasicConfig entity);
}

View File

@@ -8,7 +8,8 @@ import org.mapstruct.factory.Mappers;
/// # 支付交易转换器
///
/// 仅从 PayTrade 映射部分字段; orderNo/payBody 属容器, 由 [NormalPayAssistService#buildResult] 组装
/// 仅改名映射 orderId ← id其余同名字段自动映射orderNo/payBody 等无源字段保持 null
/// 由 [cn.daxpay.open.payment.trade.runtime.service.pay.normal.NormalPayAssistService#buildResult] 组装
@Mapper
public interface PayTradeConvert {
@@ -16,10 +17,5 @@ public interface PayTradeConvert {
/// PayTrade → NormalPayResult不含 orderNo/payBody, 需配合容器)
@Mapping(target = "orderId", source = "id")
@Mapping(target = "tradeNo", source = "tradeNo")
@Mapping(target = "orderNo", ignore = true)
@Mapping(target = "bizOrderNo", ignore = true)
@Mapping(target = "payBody", ignore = true)
@Mapping(target = "payBodyType", ignore = true)
NormalPayResult toResult(PayTrade trade);
}

View File

@@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
/// # 码牌支付(公开/H5/小程序侧)
///
/// 供 H5 码牌支付页(/code-pay/:code)扫码后调用, 无需登录态。
/// 供 H5 码牌支付页(/h/:code)扫码后调用, 无需登录态。
@IgnoreAuth
@Validated
@Tag(name = "码牌支付")

View File

@@ -6,7 +6,7 @@ import lombok.experimental.Accessors;
/// # 码牌支付信息(公开接口返回, 脱敏)
///
/// 供 H5 码牌支付页(/code-pay/:code)扫码后查询展示, 不含 appId/mchNo 等内部字段。
/// 供 H5 码牌支付页(/h/:code)扫码后查询展示, 不含 appId/mchNo 等内部字段。
@Data
@Accessors(chain = true)
@Schema(title = "码牌支付信息")

View File

@@ -1,40 +1,19 @@
package cn.daxpay.open.payment.unipay.trade.convert;
import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder;
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayOrderResult;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/// # 普通支付订单转换器(对外查询)
///
/// 将 core 双表(NormalPayOrder 业务容器 + PayTrade 资金凭证)映射为对外契约 NormalPayOrderResult
/// 资金态字段(status / outOrderNo / refundableBalance 等)取自 PayTrade, 业务字段取自 NormalPayOrder
///
/// 仅声明:改名、多源同名消歧、以及目标独有字段的 ignore由服务层后填或暂不输出
/// 仅同名映射业务容器(NormalPayOrder);资金凭证字段(tradeNo / outOrderNo / status / refundableBalance)
/// 由 [cn.daxpay.open.payment.unipay.trade.service.NormalPayOrderQueryService] 手写补充
@Mapper
public interface UnipayNormalPayOrderConvert {
UnipayNormalPayOrderConvert CONVERT = Mappers.getMapper(UnipayNormalPayOrderConvert.class);
/// NormalPayOrder + PayTrade → 对外 NormalPayOrderResult
// 业务单号取容器, 资金交易号取 trade
@Mapping(target = "orderNo", source = "order.orderNo")
@Mapping(target = "tradeNo", source = "trade.tradeNo")
// 多源同名消歧:资金态取 trade业务容器取 order
@Mapping(target = "status", source = "trade.status")
@Mapping(target = "outOrderNo", source = "trade.outOrderNo")
@Mapping(target = "refundableBalance", source = "trade.refundableBalance")
@Mapping(target = "bizOrderNo", source = "order.bizOrderNo")
@Mapping(target = "channel", source = "order.channel")
@Mapping(target = "method", source = "order.method")
@Mapping(target = "amount", source = "order.amount")
@Mapping(target = "payTime", source = "order.payTime")
@Mapping(target = "closeTime", source = "order.closeTime")
@Mapping(target = "expiredTime", source = "order.expiredTime")
// 目标独有字段:源无对应属性,显式 ignore 标明不在此转换
@Mapping(target = "realAmount", ignore = true)
@Mapping(target = "refundStatus", ignore = true)
NormalPayOrderResult toResult(NormalPayOrder order, PayTrade trade);
/// 仅同名映射业务容器;资金字段由 Service 补
NormalPayOrderResult toResult(NormalPayOrder order);
}

View File

@@ -47,9 +47,17 @@ public class NormalPayOrderQueryService {
order = normalPayOrderManager.findByBizOrderNo(param.getBizOrderNo(), param.getAppId())
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
}
// 联表资金凭证补充交易字段
// 同名映射业务容器,再联表资金凭证补充/覆盖交易字段
NormalPayOrderResult result = UnipayNormalPayOrderConvert.CONVERT.toResult(order);
PayTrade trade = payTradeManager.findByContainerId(order.getId(),
cn.daxpay.open.payment.trade.enums.PayTradeTypeEnum.NORMAL.getCode()).orElse(null);
return UnipayNormalPayOrderConvert.CONVERT.toResult(order, trade);
if (Objects.nonNull(trade)) {
result.setTradeNo(trade.getTradeNo());
result.setOutOrderNo(trade.getOutOrderNo());
// 对外契约 status = 资金态,覆盖容器业务 status
result.setStatus(trade.getStatus());
result.setRefundableBalance(trade.getRefundableBalance());
}
return result;
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5 QR code",
"mini_app": "Mini program QR code"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR code board has not been assigned to a merchant",
"idsEmpty": "Please select QR code boards",
"clientEnvNotSupport": "Current client environment does not support code-plate pay. Please scan with WeChat/Alipay etc.",
"amountRequired": "Please enter the payment amount"
"amountRequired": "Please enter the payment amount",
"programTypeNotFound": "No matching QR code program type found: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "Authorization code cannot be blank",
"size": "Authorization code cannot exceed 128 characters"
},
"programType": {
"notBlank": "Program type cannot be blank"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "Kode QR H5",
"mini_app": "Kode QR mini program"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR code board has not been assigned to a merchant",
"idsEmpty": "Please select QR code boards",
"clientEnvNotSupport": "Current client environment does not support code-plate pay. Please scan with WeChat/Alipay etc.",
"amountRequired": "Please enter the payment amount"
"amountRequired": "Please enter the payment amount",
"programTypeNotFound": "No matching QR code program type found: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "Kode otorisasi tidak boleh kosong",
"size": "Kode otorisasi tidak boleh melebihi 128 karakter"
},
"programType": {
"notBlank": "Program type cannot be blank"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5コード",
"mini_app": "ミニプログラムコード"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QRコードボードが加盟店に割り当てられていません",
"idsEmpty": "QRコードボードを選択してください",
"clientEnvNotSupport": "現在の環境ではコードプレート決済に対応していません。WeChat/Alipayなどでスキャンしてください",
"amountRequired": "支払金額を入力してください"
"amountRequired": "支払金額を入力してください",
"programTypeNotFound": "対応するコード牌プログラム種別が見つかりません: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "認証コードは必須です",
"size": "認証コードは128文字以内で入力してください"
},
"programType": {
"notBlank": "プログラム種別を入力してください"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5 코드",
"mini_app": "미니프로그램 코드"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR 코드 보드가 가맹점에 할당되지 않았습니다",
"idsEmpty": "QR 코드 보드를 선택하세요",
"clientEnvNotSupport": "현재 환경에서는 코드플레이트 결제를 지원하지 않습니다. WeChat/Alipay 등으로 스캔하세요",
"amountRequired": "결제 금액을 입력하세요"
"amountRequired": "결제 금액을 입력하세요",
"programTypeNotFound": "일치하는 코드 프로그램 유형을 찾을 수 없습니다: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "인증 코드는 필수입니다",
"size": "인증 코드는 128자를 초과할 수 없습니다"
},
"programType": {
"notBlank": "프로그램 유형은 비워 둘 수 없습니다"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "Kod QR H5",
"mini_app": "Kod QR program mini"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR code board has not been assigned to a merchant",
"idsEmpty": "Please select QR code boards",
"clientEnvNotSupport": "Current client environment does not support code-plate pay. Please scan with WeChat/Alipay etc.",
"amountRequired": "Please enter the payment amount"
"amountRequired": "Please enter the payment amount",
"programTypeNotFound": "No matching QR code program type found: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "Kod kebenaran tidak boleh kosong",
"size": "Kod kebenaran tidak boleh melebihi 128 aksara"
},
"programType": {
"notBlank": "Program type cannot be blank"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "คิวอาร์โค้ด H5",
"mini_app": "คิวอาร์โค้ดมินิโปรแกรม"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR code board has not been assigned to a merchant",
"idsEmpty": "Please select QR code boards",
"clientEnvNotSupport": "Current client environment does not support code-plate pay. Please scan with WeChat/Alipay etc.",
"amountRequired": "Please enter the payment amount"
"amountRequired": "Please enter the payment amount",
"programTypeNotFound": "No matching QR code program type found: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "รหัสอนุญาตต้องไม่เว้นว่าง",
"size": "รหัสอนุญาตต้องไม่เกิน 128 ตัวอักษร"
},
"programType": {
"notBlank": "Program type cannot be blank"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "Mã QR H5",
"mini_app": "Mã QR mini program"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "QR code board has not been assigned to a merchant",
"idsEmpty": "Please select QR code boards",
"clientEnvNotSupport": "Current client environment does not support code-plate pay. Please scan with WeChat/Alipay etc.",
"amountRequired": "Please enter the payment amount"
"amountRequired": "Please enter the payment amount",
"programTypeNotFound": "No matching QR code program type found: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "Mã ủy quyền không được để trống",
"size": "Mã ủy quyền không được vượt quá 128 ký tự"
},
"programType": {
"notBlank": "Program type cannot be blank"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5码牌",
"mini_app": "小程序码牌"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "码牌未分配商户",
"idsEmpty": "请选择码牌",
"clientEnvNotSupport": "当前打开环境不支持码牌支付, 请使用微信/支付宝等扫码",
"amountRequired": "请输入支付金额"
"amountRequired": "请输入支付金额",
"programTypeNotFound": "未找到对应的码牌程序类型: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "授权码不可为空",
"size": "授权码不可超过128位"
},
"programType": {
"notBlank": "落地程序类型不能为空"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5碼牌",
"mini_app": "小程序碼牌"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "碼牌未分配商戶",
"idsEmpty": "請選擇碼牌",
"clientEnvNotSupport": "目前開啟環境不支援碼牌支付,請使用微信/支付寶等掃碼",
"amountRequired": "請輸入支付金額"
"amountRequired": "請輸入支付金額",
"programTypeNotFound": "未找到對應的碼牌程式類型: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "授權碼不可為空",
"size": "授權碼不可超過128位"
},
"programType": {
"notBlank": "落地程式類型不能為空"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"h5": "H5碼牌",
"mini_app": "小程序碼牌"
}

View File

@@ -9,5 +9,6 @@
"notAssigned": "碼牌未分配商戶",
"idsEmpty": "請選擇碼牌",
"clientEnvNotSupport": "目前開啟環境不支援碼牌支付,請使用微信/支付寶等掃碼",
"amountRequired": "請輸入支付金額"
"amountRequired": "請輸入支付金額",
"programTypeNotFound": "未找到對應的碼牌程式類型: {0}"
}

View File

@@ -743,6 +743,9 @@
"oauthCode": {
"notBlank": "授權碼不可為空",
"size": "授權碼不可超過128位"
},
"programType": {
"notBlank": "落地程式類型不能為空"
}
}
}

View File

@@ -31,16 +31,10 @@ public interface NotifyNoticeConvert {
/// 公告转铃铛摘要(类型固定为公告, 内容摘要来自正文, 已读状态由服务层填充)
@Mapping(target = "type", constant = "notice")
@Mapping(target = "message", source = "content")
@Mapping(target = "isRead", ignore = true)
@Mapping(target = "link", ignore = true)
@Mapping(target = "htmlContent", ignore = true)
NotifyNoticeBriefResult toBrief(NotifyNotice in);
/// 个人消息转铃铛摘要(类型固定为个人消息, 内容摘要来自正文)
@Mapping(target = "type", constant = "message")
@Mapping(target = "message", source = "content")
@Mapping(target = "severity", ignore = true)
@Mapping(target = "isTop", ignore = true)
@Mapping(target = "htmlContent", ignore = true)
NotifyNoticeBriefResult convert(NotifyMessage in);
}

View File

@@ -5,7 +5,6 @@ import cn.daxpay.open.platform.system.param.mobile.MobileAppParam;
import cn.daxpay.open.platform.system.result.mobile.MobileAppResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
@@ -13,24 +12,19 @@ import org.mapstruct.factory.Mappers;
/// # 移动端应用配置外壳转换
///
/// 嵌套平台配置与 appConfig 密文由 [cn.daxpay.open.platform.system.service.mobile.MobileAppService] 处理,
/// 此处仅映射外壳字段。
/// 此处仅同名映射外壳字段。
@Mapper
public interface MobileAppConvert {
MobileAppConvert CONVERT = Mappers.getMapper(MobileAppConvert.class);
/// 实体转结果(嵌套配置由 Service 填充)
@Mapping(target = "wxMini", ignore = true)
@Mapping(target = "alipayMini", ignore = true)
@Mapping(target = "dyMini", ignore = true)
MobileAppResult toResult(MobileApp entity);
/// 参数转实体(新增, appConfig 由 Service 序列化后写入)
@Mapping(target = "appConfig", ignore = true)
MobileApp toEntity(MobileAppParam param);
/// 参数拷贝到实体(更新)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@Mapping(target = "appConfig", ignore = true)
void copy(MobileAppParam param, @MappingTarget MobileApp entity);
}