7b2401c03d30dc5c7b3d2af679d52843ffcd39bb..b51f6f0d5564b843aeb11f088873faa5aa2116ce
6 days ago Administrator
feat(mall): 为订单支付流程添加订单号生成和更新功能
b51f6f diff | tree
6 days ago Administrator
feat(pay): 添加巴西雷亚尔货币转换功能
259cba diff | tree
6 days ago Administrator
feat(system): 添加汇率配置管理功能
cf8dc9 diff | tree
6 days ago Administrator
feat(mall): 添加LWPAY支付功能支持
a2ed55 diff | tree
6 days ago Administrator
refactor(pay): 优化LWPAY签名逻辑,使用字段白名单确保安全
bd0f33 diff | tree
6 days ago Administrator
fix(pay): 修改签名字符串日志级别
e55697 diff | tree
6 days ago Administrator
fix(pay): 更新LWPAY基础地址配置
cddadc diff | tree
6 days ago Administrator
fix(common): 统一异常处理消息为英文
438e8d diff | tree
6 days ago Administrator
fix(mall): 修复Tokenview Webhook服务交易验证逻辑
492ec5 diff | tree
6 days ago Administrator
feat(address): 添加国家名称字段支持
316a91 diff | tree
6 days ago Administrator
fix(mall): 修复订单地址格式化问题
bfc887 diff | tree
6 days ago Administrator
feat(order): 添加国家配送费用配置功能
4a7433 diff | tree
6 days ago Administrator
fix(system): 修正国家运费列表页面货币单位显示
a8e4c8 diff | tree
6 days ago Administrator
fix(config): 更新测试环境数据库配置并优化国家配送列表界面
7d47e9 diff | tree
6 days ago Administrator
feat(admin): 优化国家运费配置管理功能
17e7cd diff | tree
6 days ago Administrator
feat(admin): 添加销售服务TRC20地址配置
d3642d diff | tree
6 days ago Administrator
feat(system): 添加国家运费配置功能
a834cc diff | tree
9 files added
14 files modified
910 ■■■■■ changed files
sql/mall_country_delivery.sql 40 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/handler/GlobalExceptionHandler.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java 76 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java 12 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java 99 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookService.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/dto/AddOrderDto.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/dto/AddressInfoDto.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/dto/ApiOrderPayDto.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/dto/CountryDeliveryDto.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/entity/MallAddressInfo.java 1 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/entity/MallCountryDelivery.java 33 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/mapper/MallCountryDeliveryMapper.java 16 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/IMallCountryDeliveryService.java 29 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallAddressInfoServiceImpl.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallOrderInfoServiceImpl.java 31 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/MallCountryDeliveryServiceImpl.java 109 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/vo/AddressInfoVo.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/service/LwPayService.java 41 ●●●● patch | view | raw | blame | history
src/main/resources/application-test.yml 10 ●●●● patch | view | raw | blame | history
src/main/resources/mapper/modules/MallCountryDeliveryMapper.xml 28 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/system/countryDeliveryList.html 166 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/system/moneyChange.html 146 ●●●●● patch | view | raw | blame | history
sql/mall_country_delivery.sql
New file
@@ -0,0 +1,40 @@
-- ============================================================
-- 国家运费配置表
-- ============================================================
DROP TABLE IF EXISTS mall_country_delivery;
CREATE TABLE mall_country_delivery (
    ID           BIGINT       NOT NULL AUTO_INCREMENT  COMMENT '主键',
    REVISION     INT          DEFAULT 1                COMMENT '乐观锁',
    CREATED_BY   VARCHAR(32)  DEFAULT 'system'         COMMENT '创建人',
    CREATED_TIME DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    UPDATED_BY   VARCHAR(32)  DEFAULT 'system'         COMMENT '更新人',
    UPDATED_TIME DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    COUNTRY_CODE VARCHAR(10)  NOT NULL                 COMMENT '国家编码(ISO 3166-1,如 CN/US/JP)',
    COUNTRY_NAME VARCHAR(64)  NOT NULL                 COMMENT '国家名称(中文)',
    SHIPPING_FEE DECIMAL(10,2) NOT NULL DEFAULT 0.00   COMMENT '运费(元)',
    STATUS       INT          DEFAULT 1                COMMENT '状态:1-启用,0-禁用',
    REMARK       VARCHAR(255)                          COMMENT '备注',
    PRIMARY KEY (ID),
    UNIQUE KEY uk_country_code (COUNTRY_CODE)
) COMMENT = '国家运费配置表';
-- ----------------------------
-- 默认数据
-- ----------------------------
INSERT INTO mall_country_delivery (COUNTRY_CODE, COUNTRY_NAME, SHIPPING_FEE, STATUS, REMARK) VALUES
('CN', '中国',   0.00, 1, '国内免运费'),
('US', '美国',  59.00, 1, NULL),
('JP', '日本',  35.00, 1, NULL),
('KR', '韩国',  30.00, 1, NULL),
('GB', '英国',  50.00, 1, NULL),
('DE', '德国',  50.00, 1, NULL),
('FR', '法国',  50.00, 1, NULL),
('AU', '澳大利亚', 55.00, 1, NULL),
('CA', '加拿大',  55.00, 1, NULL),
('SG', '新加坡',  28.00, 1, NULL);
-- ============================================================
-- 菜单注册 SQL(通过后台菜单管理页面操作,或直接执行)
-- ============================================================
-- INSERT INTO t_menu (MENU_NAME, URL, PERMS, ICON, TYPE, PARENT_ID, ORDER_NUM, CREATED_TIME)
-- VALUES ('国家运费设置', 'modules/system/countryDeliveryList', 'countryDelivery:view', 'layui-icon-template-1', 0, {系统设置菜单ID}, 13, NOW());
src/main/java/cc/mrbird/febs/common/handler/GlobalExceptionHandler.java
@@ -37,7 +37,7 @@
    @ExceptionHandler(value = Exception.class)
    public FebsResponse handleException(Exception e) {
        log.error("系统内部异常,异常信息", e);
        return new FebsResponse().code(HttpStatus.INTERNAL_SERVER_ERROR).message("系统繁忙,请稍后重试");
        return new FebsResponse().code(HttpStatus.INTERNAL_SERVER_ERROR).message("The system is busy, please try again later");
    }
    @ExceptionHandler(value = FebsException.class)
src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
@@ -1,13 +1,18 @@
package cc.mrbird.febs.mall.controller;
import cc.mrbird.febs.common.annotation.ControllerEndpoint;
import cc.mrbird.febs.common.controller.BaseController;
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.common.enumerates.DataDictionaryEnum;
import cc.mrbird.febs.common.utils.AppContants;
import cc.mrbird.febs.mall.dto.*;
import cc.mrbird.febs.mall.entity.DataDictionaryCustom;
import cc.mrbird.febs.mall.entity.MallCountryDelivery;
import cc.mrbird.febs.mall.entity.MallNewsInfo;
import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper;
import cc.mrbird.febs.mall.service.ICommonService;
import cc.mrbird.febs.mall.service.IMallCountryDeliveryService;
import cc.mrbird.febs.mall.service.ISystemService;
import cc.mrbird.febs.pay.model.HeaderDto;
import cc.mrbird.febs.pay.service.WxFaPiaoService;
@@ -23,9 +28,12 @@
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@@ -34,6 +42,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -42,7 +51,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/admin/system")
public class AdminSystemController {
public class AdminSystemController extends BaseController {
    @Autowired
    private ISystemService systemService;
@@ -50,6 +59,7 @@
    private final ICommonService commonService;
    private final DataDictionaryCustomMapper dataDictionaryCustomMapper;
    private final WxFaPiaoService wxFaPiaoService;
    private final IMallCountryDeliveryService countryDeliveryService;
    @PostMapping(value = "/bonusSystemSetting")
    public FebsResponse bonusSystemSetting(@RequestBody Map<String, Object> map) {
@@ -96,6 +106,7 @@
        }
        commonService.addDataDic("TOKENVIEW_CONFIG", "WEBHOOK_SECRET", dto.getWebhookSecret(), "Tokenview Webhook签名密钥", false);
        commonService.addDataDic("TOKENVIEW_CONFIG", "TRC20_USDT_ADDRESS", dto.getTrc20UsdtAddress(), "TRC20 USDT收款地址", false);
        commonService.addDataDic("SALES_SERVICE", "TRC_ADDRESS", dto.getTrc20UsdtAddress(), "TRC20 USDT收款地址", false);
        return new FebsResponse().success().message("保存成功");
    }
@@ -125,6 +136,69 @@
        return new FebsResponse().success().message("保存成功");
    }
    @GetMapping(value = "/moneyChangeList")
    public FebsResponse moneyChangeList() {
        List<DataDictionaryCustom> list = dataDictionaryCustomMapper.selectDicByType("MONEY_CHANGE");
        Map<String, Object> data = new HashMap<>();
        data.put("rows", list);
        data.put("total", list.size());
        return new FebsResponse().success().data(data);
    }
    @PostMapping(value = "/moneyChangeSave")
    @ControllerEndpoint(operation = "保存汇率配置")
    public FebsResponse moneyChangeSave(@RequestParam Map<String, String> params) {
        String id = params.get("id");
        String code = params.get("code");
        String value = params.get("value");
        String description = params.get("description");
        if (StrUtil.isBlank(code)) return new FebsResponse().fail().message("货币编码不能为空");
        if (StrUtil.isBlank(value)) return new FebsResponse().fail().message("汇率不能为空");
        if (StrUtil.isBlank(description)) return new FebsResponse().fail().message("符号不能为空");
        code = code.toUpperCase();
        // 编辑模式:先删旧记录
        if (StrUtil.isNotBlank(id)) {
            dataDictionaryCustomMapper.deleteById(Long.valueOf(id));
        }
        // 检查编码是否已存在(避免重复)
        DataDictionaryCustom exist = dataDictionaryCustomMapper.selectDicDataByTypeAndCode("MONEY_CHANGE", code);
        if (exist != null) {
            return new FebsResponse().fail().message("货币编码 " + code + " 已存在");
        }
        commonService.addDataDic("MONEY_CHANGE", code, value, description, false);
        return new FebsResponse().success().message("保存成功");
    }
    @GetMapping(value = "/moneyChangeDelete/{id}")
    @ControllerEndpoint(operation = "删除汇率配置")
    public FebsResponse moneyChangeDelete(@PathVariable Long id) {
        dataDictionaryCustomMapper.deleteById(id);
        return new FebsResponse().success().message("删除成功");
    }
    @GetMapping("countryDeliveryList")
    public FebsResponse countryDeliveryList(MallCountryDelivery dto, QueryRequest request) {
        Map<String, Object> data = getDataTable(countryDeliveryService.countryDeliveryList(dto, request));
        return new FebsResponse().success().data(data);
    }
    @PostMapping(value = "/countryDeliverySave")
    @ControllerEndpoint(operation = "保存国家运费配置")
    public FebsResponse countryDeliverySave(@Validated CountryDeliveryDto dto) {
        return countryDeliveryService.saveOrUpdate(dto);
    }
    @GetMapping(value = "/countryDeliveryDelete/{id}")
    @ControllerEndpoint(operation = "删除国家运费配置")
    public FebsResponse countryDeliveryDelete(@PathVariable Long id) {
        return countryDeliveryService.delete(id);
    }
    @PostMapping(value = "/agentAmountSetSetting")
    public FebsResponse agentAmountSetSetting(AdminAgentAmountDto adminAgentAmountDto) {
        DataDictionaryCustom dic = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
@@ -176,4 +176,16 @@
        model.addAttribute("salesService", dto);
        return FebsUtil.view("modules/system/salesService");
    }
    @GetMapping("countryDeliveryList")
    @RequiresPermissions("countryDelivery:view")
    public String countryDeliveryList() {
        return FebsUtil.view("modules/system/countryDeliveryList");
    }
    @GetMapping("moneyChange")
    @RequiresPermissions("moneyChange:update")
    public String moneyChange() {
        return FebsUtil.view("modules/system/moneyChange");
    }
}
src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java
@@ -3,14 +3,20 @@
import cc.mrbird.febs.common.annotation.Limit;
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.LimitType;
import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
import cc.mrbird.febs.common.utils.MallUtils;
import cc.mrbird.febs.common.utils.ValidateEntityUtils;
import cc.mrbird.febs.mall.controller.dependentStation.constant.OrderConstants;
import cc.mrbird.febs.mall.dto.*;
import cc.mrbird.febs.mall.entity.MallOrderInfo;
import cc.mrbird.febs.mall.service.IApiMallOrderInfoService;
import cc.mrbird.febs.mall.service.IMallCountryDeliveryService;
import cc.mrbird.febs.mall.vo.ApiOrderPayVo;
import cc.mrbird.febs.mall.vo.OrderDetailVo;
import cc.mrbird.febs.mall.vo.OrderListVo;
import cc.mrbird.febs.pay.service.IXcxPayService;
import cc.mrbird.febs.pay.service.LwPayService;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
@@ -40,6 +46,7 @@
    private final IApiMallOrderInfoService mallOrderInfoService;
    private final IXcxPayService iXcxPayService;
    private final LwPayService lwPayService;
    private final IMallCountryDeliveryService countryDeliveryService;
    @ApiOperation(value = "创建订单--验证是否允许创建", notes = "创建订单--验证是否允许创建")
    @PostMapping(value = "/createOrderVerify")
@@ -190,27 +197,76 @@
        // 2. 获取订单详情
        MallOrderInfo order = mallOrderInfoService.getById(orderId);
        if (order == null) {
            return new FebsResponse().fail().message("订单创建失败");
        }
        if (order != null
                && OrderStatusEnum.WAIT_PAY.getValue() == order.getStatus()
        ){
        // 3. 调用 LWPAY 代收接口
        try {
            String payUrl = lwPayService.createPayment(
                    order,
                    lwPayDto.getBankCode(),
                    lwPayDto.getNetwork()
            // 3. 调用 LWPAY 代收接口
            try {
                String payUrl = lwPayService.createPayment(
                        order,
                        lwPayDto.getBankCode(),
                        lwPayDto.getNetwork()
                );
                Map<String, Object> result = new HashMap<>();
                result.put("orderNo", order.getOrderNo());
                result.put("amount", order.getAmount());
                result.put("payUrl", payUrl);
                return new FebsResponse().success().data(result);
            } catch (Exception e) {
                log.error("LWPAY 代收下单失败: orderId={}", orderId, e);
                return new FebsResponse().fail().message("Payment channel exception: " + e.getMessage());
            }
        }
        return new FebsResponse().fail().message("Payment channel exception");
    }
    @ApiOperation(value = "LWPAY支付", notes = "LWPAY支付")
    @ApiResponses({
            @ApiResponse(code = 200, message = "success", response = ApiOrderPayVo.class)
    })
    @PostMapping(value = "/payOrderByLwPay", produces = "application/json")
    public FebsResponse payOrderByLwPay(@RequestBody @Validated ApiOrderPayDto payDto) {
        Long orderId = payDto.getOrderId();
        Integer payType = payDto.getPayType();
        // 2. 获取订单详情
        MallOrderInfo order = mallOrderInfoService.getById(orderId);
        if (order != null
                && OrderConstants.PAY_TYPE_SYSTEM == payType
                && OrderStatusEnum.WAIT_PAY.getValue() == order.getStatus()
        ){
            String orderNo = MallUtils.getOrderNum();
            order.setOrderNo(orderNo);
            mallOrderInfoService.getBaseMapper().update(
                    null,
                    Wrappers.lambdaUpdate(MallOrderInfo.class)
                    .set(MallOrderInfo::getOrderNo, orderNo)
                    .eq(MallOrderInfo::getId, orderId)
            );
            // 3. 调用 LWPAY 代收接口
            try {
                String payUrl = lwPayService.createPayment(
                        order,
                        "967",
                        null
                );
            Map<String, Object> result = new HashMap<>();
            result.put("orderNo", order.getOrderNo());
            result.put("amount", order.getAmount());
            result.put("payUrl", payUrl);
            return new FebsResponse().success().data(result).message("下单成功");
        } catch (Exception e) {
            log.error("LWPAY 代收下单失败: orderId={}", orderId, e);
            return new FebsResponse().fail().message("支付通道异常: " + e.getMessage());
                Map<String, Object> result = new HashMap<>();
                result.put("orderNo", order.getOrderNo());
                result.put("amount", order.getAmount());
                result.put("payUrl", payUrl);
                return new FebsResponse().success().data(result).message("下单成功");
            } catch (Exception e) {
                log.error("LWPAY 代收下单失败: orderId={}", orderId, e);
                return new FebsResponse().fail().message("Payment channel exception: " + e.getMessage());
            }
        }
        return new FebsResponse().fail().message("Payment channel exception");
    }
    /**
@@ -254,4 +310,13 @@
        return "redirect:/pages/payResult?orderNo=" + orderId + "&code=" + returncode;
    }
    @ApiOperation(value = "根据国家编码查询运费", notes = "根据国家编码查询对应运费")
    @ApiResponses({
            @ApiResponse(code = 200, message = "success")
    })
    @GetMapping(value = "/getShippingFee")
    public FebsResponse getShippingFee(@RequestParam String countryCode) {
        return countryDeliveryService.getShippingFeeByCountryCode(countryCode);
    }
}
src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookService.java
@@ -68,15 +68,23 @@
        }
        // 从 payload 提取关键字段(兼容多种 key 命名)
        String toAddress = getField(payload, "address", "toAddress","to");
        String txid = getField(payload, "txid", "hash", "transactionHash");
        String toAddress = getField(payload, "to", "toAddress");
        String time = getField(payload, "time");
        String value = getField(payload, "value", "amount");
        String tokenAddr = getField(payload, "token", "tokenAddr", "contractAddress");
        String symbol = getField(payload, "symbol", "tokenSymbol");
        String coin = getField(payload, "coin");
        String tokenAddr = getField(payload, "token", "tokenAddr", "contractAddress","tokenAddress");
        String tokenSymbol = getField(payload, "tokenSymbol");
        String tokenValue = getField(payload, "tokenValue");
        if (StrUtil.isBlank(txid)) {
            log.warn("webhook 推送数据缺少交易哈希 txid: {}", payload);
            return "缺少 txid,跳过处理";
        }
        if (StrUtil.isNotBlank(tokenSymbol) && !"USDT".equalsIgnoreCase(tokenSymbol)) {
            log.info("交易币种不匹配,期望: {}, 实际: {}, txid: {}", "USDT", tokenSymbol, txid);
            return "收款地址不匹配,跳过处理";
        }
        // 验证收款地址是否匹配
@@ -87,12 +95,12 @@
        // 验证 token 合约地址(USDT: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t)
        if (StrUtil.isNotBlank(tokenAddr) && !"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".equalsIgnoreCase(tokenAddr)) {
            log.info("非 USDT 代币交易,跳过处理: token={}, symbol={}, txid={}", tokenAddr, symbol, txid);
            log.info("非 USDT 代币交易,跳过处理: token={}, symbol={}, txid={}", tokenAddr, tokenSymbol, txid);
            return "非 USDT 交易,跳过处理";
        }
        // 执行充值匹配
        return matchAndUpdateOrder(txid, value, receiveAddress);
        return matchAndUpdateOrder(txid, tokenValue, receiveAddress);
    }
    /**
@@ -133,8 +141,9 @@
        }
        BigDecimal amount;
        try {
            amount = new BigDecimal(rawValue)
                    .divide(new BigDecimal("1000000"), 2, RoundingMode.DOWN);
//            amount = new BigDecimal(rawValue)
//                    .divide(new BigDecimal("1000000"), 2, RoundingMode.DOWN);
            amount = new BigDecimal(rawValue);
        } catch (NumberFormatException e) {
            log.error("金额解析失败: txid={}, value={}", txid, rawValue, e);
            return "金额解析失败: " + txid;
src/main/java/cc/mrbird/febs/mall/dto/AddOrderDto.java
@@ -29,6 +29,9 @@
    @ApiModelProperty(value = "配送费")
    private BigDecimal deliveryAmount;
    @ApiModelProperty(value = "国家编码")
    private String countryCode;
    @ApiModelProperty(value = "1-普通订单/2-积分订单")
    private Integer orderType;
src/main/java/cc/mrbird/febs/mall/dto/AddressInfoDto.java
@@ -48,6 +48,8 @@
    @NotBlank(message = "Country/Region required")
    @ApiModelProperty(value = "Country/Region")
    private String country;
    @ApiModelProperty(value = "Country/Region")
    private String countryName;
    /**
     * 经度
src/main/java/cc/mrbird/febs/mall/dto/ApiOrderPayDto.java
@@ -17,6 +17,6 @@
    private Long orderId;
    @NotNull(message = "支付方式不能为空")
    @ApiModelProperty(value = "支付方式 1-微信 2-支付宝 3-USDT", example = "you_ke_*****")
    @ApiModelProperty(value = "支付方式 1-微信 2-支付宝 3-USDT 4-LwPAY", example = "you_ke_*****")
    private Integer payType;
}
src/main/java/cc/mrbird/febs/mall/dto/CountryDeliveryDto.java
New file
@@ -0,0 +1,36 @@
package cc.mrbird.febs.mall.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
 * 国家运费配置 DTO
 *
 * @author auto-generated
 */
@Data
public class CountryDeliveryDto {
    private Long id;
    /** 国家编码 */
    @NotBlank(message = "国家编码不能为空")
    private String countryCode;
    /** 国家名称 */
    @NotBlank(message = "国家名称不能为空")
    private String countryName;
    /** 运费 */
    @NotNull(message = "运费不能为空")
    private BigDecimal shippingFee;
    /** 状态 */
    private Integer status;
    /** 备注 */
    private String remark;
}
src/main/java/cc/mrbird/febs/mall/entity/MallAddressInfo.java
@@ -52,5 +52,6 @@
    //市
    private String city;
    private String country;
    private String countryName;
}
src/main/java/cc/mrbird/febs/mall/entity/MallCountryDelivery.java
New file
@@ -0,0 +1,33 @@
package cc.mrbird.febs.mall.entity;
import cc.mrbird.febs.common.entity.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 国家运费配置表
 *
 * @author auto-generated
 * @date 2026-06-22
 */
@Data
@TableName("mall_country_delivery")
public class MallCountryDelivery extends BaseEntity {
    /** 国家编码(如 CN, US, JP) */
    private String countryCode;
    /** 国家名称(中文) */
    private String countryName;
    /** 运费(元) */
    private BigDecimal shippingFee;
    /** 状态:1-启用,0-禁用 */
    private Integer status;
    /** 备注 */
    private String remark;
}
src/main/java/cc/mrbird/febs/mall/mapper/MallCountryDeliveryMapper.java
New file
@@ -0,0 +1,16 @@
package cc.mrbird.febs.mall.mapper;
import cc.mrbird.febs.mall.entity.MallCountryDelivery;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
 * 国家运费配置 Mapper
 *
 * @author auto-generated
 */
public interface MallCountryDeliveryMapper extends BaseMapper<MallCountryDelivery> {
    IPage<MallCountryDelivery> getCountryDeliveryListInPage(Page<MallCountryDelivery> page, MallCountryDelivery dto);
}
src/main/java/cc/mrbird/febs/mall/service/IMallCountryDeliveryService.java
New file
@@ -0,0 +1,29 @@
package cc.mrbird.febs.mall.service;
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.mall.dto.CountryDeliveryDto;
import cc.mrbird.febs.mall.entity.MallCountryDelivery;
import com.baomidou.mybatisplus.core.metadata.IPage;
/**
 * 国家运费配置 Service 接口
 *
 * @author auto-generated
 */
public interface IMallCountryDeliveryService {
    /** 列表查询 */
    FebsResponse list();
    IPage<MallCountryDelivery> countryDeliveryList(MallCountryDelivery dto, QueryRequest request);
    /** 新增/更新 */
    FebsResponse saveOrUpdate(CountryDeliveryDto dto);
    /** 删除 */
    FebsResponse delete(Long id);
    /** 根据国家编码查询运费 */
    FebsResponse getShippingFeeByCountryCode(String countryCode);
}
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallAddressInfoServiceImpl.java
@@ -70,6 +70,7 @@
            addressInfoVo.setCity(mallAddressInfo.getCity());
            addressInfoVo.setProvince(mallAddressInfo.getProvince());
            addressInfoVo.setCountry(mallAddressInfo.getCountry());
            addressInfoVo.setCountryName(mallAddressInfo.getCountryName());
        }
        return new FebsResponse().success().data(addressInfoVo);
    }
@@ -92,6 +93,7 @@
                        .set(MallAddressInfo::getCity, addressInfoDto.getCity())
                        .set(MallAddressInfo::getProvince, addressInfoDto.getProvince())
                        .set(MallAddressInfo::getCountry, addressInfoDto.getCountry())
                        .set(MallAddressInfo::getCountryName, addressInfoDto.getCountryName())
                        .eq(MallAddressInfo::getMemberId, memberId)
        );
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallOrderInfoServiceImpl.java
@@ -71,6 +71,7 @@
    private final IApiMallMemberService memberService;
    private final IMallMoneyFlowService mallMoneyFlowService;
    private final RedisUtils redisUtils;
    private final MallCountryDeliveryMapper mallCountryDeliveryMapper;
    private final AgentProducer agentProducer;
    private final ApiChatPayService apiChatPayService;
@@ -116,8 +117,6 @@
        this.baseMapper.insert(orderInfo);
        BigDecimal total = BigDecimal.ZERO;
        //运费
        BigDecimal delivaryAmount = addOrderDto.getDeliveryAmount() == null ? BigDecimal.ZERO : addOrderDto.getDeliveryAmount();
        for (AddOrderItemDto item : addOrderDto.getItems()) {
            MallOrderItem orderItem = new MallOrderItem();
@@ -140,8 +139,6 @@
            if(1 != goodsResult){
                throw new FebsException("Discontinued");
            }
            delivaryAmount = delivaryAmount.add(mallGoods.getCarriageAmount());
            BigDecimal amount = sku.getPresentPrice().multiply(BigDecimal.valueOf(item.getCnt()));
            orderItem.setAmount(amount);
@@ -167,6 +164,24 @@
            }
            mallOrderItemMapper.insert(orderItem);
        }
        //运费
        MallCountryDelivery delivery = mallCountryDeliveryMapper.selectOne(
                Wrappers.lambdaQuery(MallCountryDelivery.class)
                        .eq(MallCountryDelivery::getCountryCode, addOrderDto.getCountryCode().toUpperCase())
                        .eq(MallCountryDelivery::getStatus, 1)
        );
        MallCountryDelivery defaultDelivery = mallCountryDeliveryMapper.selectOne(
                Wrappers.lambdaQuery(MallCountryDelivery.class)
                        .eq(MallCountryDelivery::getCountryCode, "DEFAULT")
                        .eq(MallCountryDelivery::getStatus, 1)
        );
        BigDecimal delivaryAmount = defaultDelivery.getShippingFee();
        if (delivery != null) {
            delivaryAmount = delivery.getShippingFee();
        }
        orderInfo.setCarriage(delivaryAmount);
        total = total.add(delivaryAmount);
@@ -175,7 +190,13 @@
        orderInfo.setName(address.getFristName() + address.getName());
        orderInfo.setPhone(address.getPhone());
        orderInfo.setAddress(address.getArea()+ address.getAddress()+address.getCity()+address.getProvince() + address.getCountry() );
        orderInfo.setAddress(
                        address.getAddress() +"  -  "
                        +address.getArea() +"  -  "
                        +address.getCity() +"  -  "
                        +address.getProvince() +"  -  "
                        +address.getCountryName() +"  -  "
                        + address.getCountry() );
        orderInfo.setLatitude(address.getLatitude());
        orderInfo.setLongitude(address.getLongitude());
        this.baseMapper.updateById(orderInfo);
src/main/java/cc/mrbird/febs/mall/service/impl/MallCountryDeliveryServiceImpl.java
New file
@@ -0,0 +1,109 @@
package cc.mrbird.febs.mall.service.impl;
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.mall.dto.CountryDeliveryDto;
import cc.mrbird.febs.mall.entity.MallCountryDelivery;
import cc.mrbird.febs.mall.mapper.MallCountryDeliveryMapper;
import cc.mrbird.febs.mall.service.IMallCountryDeliveryService;
import cc.mrbird.febs.mall.vo.AdminMallNewsInfoVo;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
/**
 * 国家运费配置 Service 实现
 *
 * @author auto-generated
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MallCountryDeliveryServiceImpl implements IMallCountryDeliveryService {
    private final MallCountryDeliveryMapper mallCountryDeliveryMapper;
    @Override
    public FebsResponse list() {
        List<MallCountryDelivery> list = mallCountryDeliveryMapper.selectList(
                Wrappers.lambdaQuery(MallCountryDelivery.class)
                        .orderByAsc(MallCountryDelivery::getCountryCode)
        );
        return new FebsResponse().success().data(list);
    }
    @Override
    public IPage<MallCountryDelivery> countryDeliveryList(MallCountryDelivery dto, QueryRequest request) {
        Page<MallCountryDelivery> page = new Page<>(request.getPageNum(), request.getPageSize());
        IPage<MallCountryDelivery> pages = mallCountryDeliveryMapper.getCountryDeliveryListInPage(page, dto);
        return pages;
    }
    @Override
    public FebsResponse saveOrUpdate(CountryDeliveryDto dto) {
        MallCountryDelivery entity = new MallCountryDelivery();
        entity.setCountryCode(dto.getCountryCode().toUpperCase());
        entity.setCountryName(dto.getCountryName());
        entity.setShippingFee(dto.getShippingFee());
        entity.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
        entity.setRemark(dto.getRemark());
        if (dto.getId() != null) {
            // 更新
            entity.setId(dto.getId());
            mallCountryDeliveryMapper.updateById(entity);
            log.info("更新国家运费配置: countryCode={}, fee={}", entity.getCountryCode(), entity.getShippingFee());
        } else {
            // 新增前检查编码是否重复
            MallCountryDelivery exist = mallCountryDeliveryMapper.selectOne(
                    Wrappers.lambdaQuery(MallCountryDelivery.class)
                            .eq(MallCountryDelivery::getCountryCode, entity.getCountryCode())
            );
            if (exist != null) {
                return new FebsResponse().fail().message("国家编码 " + entity.getCountryCode() + " 已存在");
            }
            mallCountryDeliveryMapper.insert(entity);
            log.info("新增国家运费配置: countryCode={}, fee={}", entity.getCountryCode(), entity.getShippingFee());
        }
        return new FebsResponse().success().message("保存成功");
    }
    @Override
    public FebsResponse delete(Long id) {
        mallCountryDeliveryMapper.deleteById(id);
        return new FebsResponse().success().message("删除成功");
    }
    @Override
    public FebsResponse getShippingFeeByCountryCode(String countryCode) {
        if (StrUtil.isBlank(countryCode)) {
            return new FebsResponse().fail().message("国家编码不能为空");
        }
        MallCountryDelivery delivery = mallCountryDeliveryMapper.selectOne(
                Wrappers.lambdaQuery(MallCountryDelivery.class)
                        .eq(MallCountryDelivery::getCountryCode, countryCode.toUpperCase())
                        .eq(MallCountryDelivery::getStatus, 1)
        );
        if (delivery == null) {
            MallCountryDelivery defaultDelivery = mallCountryDeliveryMapper.selectOne(
                    Wrappers.lambdaQuery(MallCountryDelivery.class)
                            .eq(MallCountryDelivery::getCountryCode, "DEFAULT")
                            .eq(MallCountryDelivery::getStatus, 1)
            );
            // 没有配置则返回默认运费 0(可根据业务调整)
            return new FebsResponse().success().data(defaultDelivery.getShippingFee());
        }
        return new FebsResponse().success().data(delivery.getShippingFee());
    }
}
src/main/java/cc/mrbird/febs/mall/vo/AddressInfoVo.java
@@ -40,6 +40,9 @@
    @ApiModelProperty(value = "Country/Region")
    private String country;
    private String countryName;
    /**
     * 经度
     */
src/main/java/cc/mrbird/febs/pay/service/LwPayService.java
@@ -14,6 +14,8 @@
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
@@ -44,7 +46,16 @@
    /** LWPAY 配置字典 type */
    private static final String DICT_TYPE = "LWPAY_CONFIG";
    /** LWPAY API 基础地址 */
    private static final String LWPAY_BASE_URL = "https://www.lwpay.com";
    private static final String LWPAY_BASE_URL = "https://lwpay.live";
    /**
     * 参与签名的字段白名单
     * 签名只包含这 7 个业务必传字段,pay_productname / pay_attach / pay_md5sign 不参与签名
     */
    private static final String[] SIGN_FIELD_KEYS = {
            "pay_memberid", "pay_orderid", "pay_applydate",
            "pay_bankcode", "pay_notifyurl", "pay_callbackurl", "pay_amount"
    };
    // ==================== 代收接口 ====================
@@ -70,7 +81,7 @@
        params.put("pay_bankcode", bankCode);
        params.put("pay_notifyurl", getNotifyUrl());
        params.put("pay_callbackurl", getReturnUrl());
        params.put("pay_amount", order.getAmount().toPlainString());
        params.put("pay_amount", getBRLAmount(order.getAmount()));
        params.put("pay_productname", "商品订单-" + order.getOrderNo());
        // 通道 968 需要 network 参数
@@ -78,8 +89,15 @@
            params.put("pay_attach", "network:" + network);
        }
        // 生成签名
        String sign = generateSign(params, secretKey);
        // 签名:仅使用白名单中的 7 个业务必传字段
        TreeMap<String, String> signParams = new TreeMap<>();
        for (String key : SIGN_FIELD_KEYS) {
            String val = params.get(key);
            if (StrUtil.isNotBlank(val)) {
                signParams.put(key, val);
            }
        }
        String sign = generateSign(signParams, secretKey);
        params.put("pay_md5sign", sign);
        log.info("LWPAY 代收请求: memberId={}, orderNo={}, amount={}, bankCode={}",
@@ -100,6 +118,19 @@
        }
        return json.getString("payurl");
    }
    private String getBRLAmount(BigDecimal amount){
        DataDictionaryCustom dataDictionaryCustom = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
                "MONEY_CHANGE",
                "BRL"
        );
        BigDecimal rate = new BigDecimal("5.18");
        if (dataDictionaryCustom != null){
            rate = new BigDecimal(dataDictionaryCustom.getValue());
        }
        return  amount.multiply( rate).setScale(2, RoundingMode.DOWN).toString();
    }
    // ==================== 回调处理 ====================
@@ -186,7 +217,7 @@
     */
    public String generateSign(TreeMap<String, String> params, String secretKey) {
        String signStr = buildSignString(params) + "&key=" + secretKey;
        log.debug("LWPAY 待签名字符串: {}", signStr);
        log.info("LWPAY 待签名字符串: {}", signStr);
        return SecureUtil.md5(signStr).toUpperCase();
    }
src/main/resources/application-test.yml
@@ -17,10 +17,14 @@
      datasource:
        # 数据源-1,名称为 base
        base:
          username: db_e2
          password: db_e20806123!@#
#          username: db_e2
#          password: db_e20806123!@#
#          driver-class-name: com.mysql.cj.jdbc.Driver
#          url: jdbc:mysql://120.27.238.55:3406/db_e2?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
          username: root
          password: 1!qaz2@WSX
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://120.27.238.55:3406/db_e2?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
          url: jdbc:mysql://159.198.39.140:3306/db_e2?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
  redis:
    # Redis数据库索引(默认为 0)
src/main/resources/mapper/modules/MallCountryDeliveryMapper.xml
New file
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cc.mrbird.febs.mall.mapper.MallCountryDeliveryMapper">
    <resultMap id="BaseResultMap" type="cc.mrbird.febs.mall.entity.MallCountryDelivery">
        <id column="ID" jdbcType="BIGINT" property="id"/>
        <result column="REVISION" jdbcType="INTEGER" property="revision"/>
        <result column="CREATED_BY" jdbcType="VARCHAR" property="createdBy"/>
        <result column="CREATED_TIME" jdbcType="TIMESTAMP" property="createdTime"/>
        <result column="UPDATED_BY" jdbcType="VARCHAR" property="updatedBy"/>
        <result column="UPDATED_TIME" jdbcType="TIMESTAMP" property="updatedTime"/>
        <result column="COUNTRY_CODE" jdbcType="VARCHAR" property="countryCode"/>
        <result column="COUNTRY_NAME" jdbcType="VARCHAR" property="countryName"/>
        <result column="SHIPPING_FEE" jdbcType="DECIMAL" property="shippingFee"/>
        <result column="STATUS" jdbcType="INTEGER" property="status"/>
        <result column="REMARK" jdbcType="VARCHAR" property="remark"/>
    </resultMap>
    <select id="getCountryDeliveryListInPage" resultType="cc.mrbird.febs.mall.entity.MallCountryDelivery">
        SELECT
            a.*
        FROM mall_country_delivery a
        order by a.CREATED_TIME desc
    </select>
</mapper>
src/main/resources/templates/febs/views/modules/system/countryDeliveryList.html
New file
@@ -0,0 +1,166 @@
<div class="layui-fluid layui-anim febs-anim" id="country-delivery-list" lay-title="国家运费设置">
    <div class="layui-row febs-container">
        <div class="layui-col-md12">
            <div class="layui-card">
                <div class="layui-card-body febs-table-full">
                    <form class="layui-form layui-table-form" lay-filter="country-delivery-table-form">
                        <div class="layui-row">
                            <div class="layui-col-md10">
                                <div class="layui-form-item">
                                    <button class="layui-btn layui-btn-normal" type="button" id="add-btn">
                                        <i class="layui-icon">&#xe654;</i> 新增
                                    </button>
                                </div>
                            </div>
                            <div class="layui-col-md2 layui-col-sm12 layui-col-xs12 table-action-area">
                                <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain table-action" id="reset">
                                    <i class="layui-icon">&#xe79b;</i>
                                </div>
                            </div>
                        </div>
                    </form>
                    <table lay-filter="countryDeliveryTable" lay-data="{id: 'countryDeliveryTable'}"></table>
                </div>
            </div>
        </div>
    </div>
</div>
<!-- 表格操作栏 -->
<script type="text/html" id="country-delivery-option">
    <a lay-event="edit"><i class="layui-icon febs-edit-area febs-blue">&#xe7a5;</i></a>
    <a lay-event="del"><i class="layui-icon febs-edit-area febs-red">&#xe640;</i></a>
</script>
<style>
    .layui-form-label { width: 120px; }
    .layui-form-item .layui-input-block { margin-left: 150px; }
</style>
<script data-th-inline="none" type="text/javascript">
    layui.use(['jquery', 'form', 'table', 'febs'], function () {
        var $ = layui.jquery,
            febs = layui.febs,
            form = layui.form,
            table = layui.table,
            layer = layui.layer,
            $view = $('#country-delivery-list'),
            $reset = $view.find('#reset'),
            $addBtn = $view.find('#add-btn'),
            tableIns;
        form.render();
        initTable();
        // 表格操作
        table.on('tool(countryDeliveryTable)', function (obj) {
            var data = obj.data,
                layEvent = obj.event;
            if (layEvent === 'edit') {
                openEditModal(data);
            } else if (layEvent === 'del') {
                layer.confirm('确定删除该国家的运费配置吗?', function (index) {
                    febs.get(ctx + 'admin/system/countryDeliveryDelete/' + data.id, {}, function () {
                        layer.close(index);
                        febs.alert.success('删除成功');
                        tableIns.reload();
                    });
                });
            }
        });
        // 新增按钮
        $addBtn.on('click', function () {
            openEditModal(null);
        });
        // 刷新按钮
        $reset.on('click', function () {
            tableIns.reload();
        });
        // 初始化表格
        function initTable() {
            tableIns = febs.table.init({
                elem: $view.find('table'),
                id: 'countryDeliveryTable',
                url: ctx + 'admin/system/countryDeliveryList',
                cols: [[
                    {field: 'countryCode', title: '国家编码', width: 120, align: 'center'},
                    {field: 'countryName', title: '国家名称', width: 150, align: 'center'},
                    {field: 'shippingFee', title: '运费($)', width: 120, align: 'center'},
                    {field: 'status', title: '状态', width: 100, align: 'center',
                        templet: function (d) {
                            return d.status === 1
                                ? '<span style="color:#5FB878;">● 启用</span>'
                                : '<span style="color:#999;">● 禁用</span>';
                        }
                    },
                    {title: '操作', templet: '#country-delivery-option', width: 120, align: 'center'},
                    {field: 'remark', title: '备注', minWidth: 100, align: 'center'},
                ]]
            });
        }
        // 打开编辑弹窗
        function openEditModal(data) {
            var title = data ? '编辑国家运费' : '新增国家运费';
            var html = '<div style="padding:20px 25px 0 0;">' +
                '<form class="layui-form" lay-filter="country-delivery-form">' +
                (data ? '<div class="layui-form-item febs-hide"><label class="layui-form-label">ID</label><div class="layui-input-block"><input type="text" name="id" value="' + data.id + '"></div></div>' : '') +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">国家编码</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="countryCode" class="layui-input" ' + (data ? 'value="' + data.countryCode + '"' : 'placeholder="如 CN, US, JP"') + ' lay-verify="required">' +
                '      <div class="layui-form-mid layui-word-aux">ISO国家编码,如CN/US/JP</div>' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">国家名称</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="countryName" class="layui-input" ' + (data ? 'value="' + (data.countryName || '') + '"' : 'placeholder="中文名称"') + ' lay-verify="required">' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">运费($)</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="number" name="shippingFee" class="layui-input" ' + (data ? 'value="' + data.shippingFee + '"' : 'placeholder="运费金额"') + ' lay-verify="required|number">' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label">状态</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="radio" name="status" value="1" title="启用" ' + (!data || data.status === 1 ? 'checked' : '') + '>' +
                '      <input type="radio" name="status" value="0" title="禁用" ' + (data && data.status === 0 ? 'checked' : '') + '>' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label">备注</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="remark" class="layui-input" ' + (data && data.remark ? 'value="' + data.remark + '"' : '') + '>' +
                '    </div>' +
                '  </div>' +
                '</form></div>';
            var layerIndex = layer.open({
                type: 1,
                title: title,
                area: ['500px', '420px'],
                content: html,
                success: function (layero) {
                    form.render('radio', 'country-delivery-form');
                },
                btn: ['保存', '取消'],
                yes: function (index, layero) {
                    var formObj = $(layero).find('form');
                    var data = form.val("country-delivery-form");
                    febs.post(ctx + 'admin/system/countryDeliverySave', data, function (res) {
                        layer.close(index);
                        febs.alert.success('保存成功');
                        tableIns.reload();
                    });
                },
                btn2: function () {
                    layer.closeAll();
                }
            });
        }
    });
</script>
src/main/resources/templates/febs/views/modules/system/moneyChange.html
New file
@@ -0,0 +1,146 @@
<div class="layui-fluid layui-anim febs-anim" id="money-change-list" lay-title="汇率设置">
    <div class="layui-row febs-container">
        <div class="layui-col-md12">
            <div class="layui-card">
                <div class="layui-card-body febs-table-full">
                    <form class="layui-form layui-table-form" lay-filter="money-change-table-form">
                        <div class="layui-row">
                            <div class="layui-col-md10">
                                <div class="layui-form-item">
                                    <button class="layui-btn layui-btn-normal" type="button" id="add-btn">
                                        <i class="layui-icon">&#xe654;</i> 新增
                                    </button>
                                </div>
                            </div>
                            <div class="layui-col-md2 layui-col-sm12 layui-col-xs12 table-action-area">
                                <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain table-action" id="reset">
                                    <i class="layui-icon">&#xe79b;</i>
                                </div>
                            </div>
                        </div>
                    </form>
                    <table lay-filter="moneyChangeTable" lay-data="{id: 'moneyChangeTable'}"></table>
                </div>
            </div>
        </div>
    </div>
</div>
<!-- 表格操作栏 -->
<script type="text/html" id="money-change-option">
    <a lay-event="edit"><i class="layui-icon febs-edit-area febs-blue">&#xe7a5;</i></a>
    <a lay-event="del"><i class="layui-icon febs-edit-area febs-red">&#xe640;</i></a>
</script>
<style>
    .layui-form-label { width: 120px; }
    .layui-form-item .layui-input-block { margin-left: 150px; }
</style>
<script data-th-inline="none" type="text/javascript">
    layui.use(['jquery', 'form', 'table', 'febs'], function () {
        var $ = layui.jquery,
            febs = layui.febs,
            form = layui.form,
            table = layui.table,
            layer = layui.layer,
            $view = $('#money-change-list'),
            $reset = $view.find('#reset'),
            $addBtn = $view.find('#add-btn'),
            tableIns;
        form.render();
        initTable();
        // 表格操作
        table.on('tool(moneyChangeTable)', function (obj) {
            var data = obj.data,
                layEvent = obj.event;
            if (layEvent === 'edit') {
                openEditModal(data);
            } else if (layEvent === 'del') {
                layer.confirm('确定删除 ' + data.code + ' 的汇率配置吗?', function (index) {
                    febs.get(ctx + 'admin/system/moneyChangeDelete/' + data.id, {}, function () {
                        layer.close(index);
                        febs.alert.success('删除成功');
                        tableIns.reload();
                    });
                });
            }
        });
        // 新增按钮
        $addBtn.on('click', function () {
            openEditModal(null);
        });
        // 刷新按钮
        $reset.on('click', function () {
            tableIns.reload();
        });
        // 初始化表格
        function initTable() {
            tableIns = febs.table.init({
                elem: $view.find('table'),
                id: 'moneyChangeTable',
                url: ctx + 'admin/system/moneyChangeList',
                cols: [[
                    {field: 'code', title: '货币编码', width: 120, align: 'center'},
                    {field: 'value', title: '汇率', width: 120, align: 'center'},
                    {field: 'description', title: '符号', width: 80, align: 'center'},
                    {title: '操作', templet: '#money-change-option', width: 120, align: 'center'}
                ]]
            });
        }
        // 打开编辑弹窗
        function openEditModal(data) {
            var title = data ? '编辑汇率' : '新增汇率';
            var html = '<div style="padding:20px 25px 0 0;">' +
                '<form class="layui-form" lay-filter="money-change-form">' +
                (data ? '<div class="layui-form-item febs-hide"><label class="layui-form-label">ID</label><div class="layui-input-block"><input type="text" name="id" value="' + data.id + '"></div></div>' : '') +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">货币编码</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="code" class="layui-input" ' + (data ? 'value="' + data.code + '"' : 'placeholder="如 USD, CNY, EUR"') + ' lay-verify="required">' +
                '      <div class="layui-form-mid layui-word-aux">ISO货币编码,如USD/CNY/EUR</div>' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">汇率</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="value" class="layui-input" ' + (data ? 'value="' + data.value + '"' : 'placeholder="汇率数值"') + ' lay-verify="required|number">' +
                '    </div>' +
                '  </div>' +
                '  <div class="layui-form-item">' +
                '    <label class="layui-form-label febs-form-item-require">符号</label>' +
                '    <div class="layui-input-block">' +
                '      <input type="text" name="description" class="layui-input" ' + (data && data.description ? 'value="' + data.description + '"' : 'placeholder="$"') + ' lay-verify="required">' +
                '      <div class="layui-form-mid layui-word-aux">货币符号,如 $ ¥ € £</div>' +
                '    </div>' +
                '  </div>' +
                '</form></div>';
            layer.open({
                type: 1,
                title: title,
                area: ['500px', '350px'],
                content: html,
                success: function (layero) {
                    form.render(null, 'money-change-form');
                },
                btn: ['保存', '取消'],
                yes: function (index, layero) {
                    var formObj = $(layero).find('form');
                    var formData = form.val("money-change-form");
                    febs.post(ctx + 'admin/system/moneyChangeSave', formData, function (res) {
                        layer.close(index);
                        febs.alert.success('保存成功');
                        tableIns.reload();
                    });
                },
                btn2: function () {
                    layer.closeAll();
                }
            });
        }
    });
</script>