Files
shop-admin/docs/coupon-backend-upgrade.md
赵忠林 de93292fa2 feat(shop): 新增商城基础设置组件
- 新增商城基础信息配置界面支持店铺名称、Logo、描述、电话、地址和开关配置
- 实现图片选择和删除功能,支持Logo的上传回显
- 集成表单校验和保存接口调用,提供保存状态反馈
- 优化响应式布局适配不同屏幕尺寸

fix(cms): 防止文章编辑内容的XSS攻击

- 在文章编辑组件中对动态HTML内容添加DOMPurify消毒
- 替换 v-html 渲染为安全消毒后的内容展现
- 确保富文本内容安全,防止跨站脚本漏洞

refactor(system-setting): 优化系统设置基本信息组件逻辑

- 替换ico文件上传组件,改用SelectFile实现图片选择和删除功能
- 简化图标上传流程,移除上传接口调用相关代码
- 统一表单数据处理,增强设置数据解析和回显兼容性
- 调整保存逻辑,支持根据是否存在主键调用新增或更新接口
- 改进watch数据响应逻辑,支持多种数据结构兼容

fix(system-setting): 修正清理设置组件数据重置逻辑

- 统一清理设置组件的 settingKey 值为 clear,避免混淆
- 优化数据监听回调,支持不同数据结构和空数据重置表单
- 确保组件初始化状态正确,避免遗留数据影响展示

fix(store): 修正 chat store 定义方式

- 按 pinia 官方规范简化 store 定义参数
- 修复 store id 错误传递问题,确保正确注册和使用
2026-06-16 12:52:30 +08:00

7.9 KiB
Raw Permalink Blame History

优惠券模块后端改造方案

前端 Vue 改造已完成paopao-vue本文档为后端 Java 端对应的改动清单。


一、DDL — shop_coupon 表新增字段

-- ============================================================
-- 优惠券模块升级:新增发放对象控制 + 场地使用券支持
-- 执行前请先备份表数据!
-- ============================================================

ALTER TABLE `shop_coupon`
  -- 发放对象(0全部用户 1仅会员 2仅非会员 3指定用户)
  ADD COLUMN `receive_target` TINYINT NOT NULL DEFAULT 0 COMMENT '发放对象(0全部用户 1仅会员 2仅非会员 3指定用户)' AFTER `enabled`,
  -- 指定用户ID列表(JSON数组)receive_target=3 时使用
  ADD COLUMN `receive_user_ids` VARCHAR(1000) DEFAULT NULL COMMENT '指定用户ID列表(JSON数组格式)receiveTarget=3时使用' AFTER `receive_target`,
  -- 场地使用券相关字段 (type=50 时使用)
  ADD COLUMN `venue_type` TINYINT DEFAULT NULL COMMENT '场地使用券-场地类型' AFTER `receive_user_ids`,
  ADD COLUMN `venue_id` INT DEFAULT NULL COMMENT '场地使用券-指定场地ID' AFTER `venue_type`,
  ADD COLUMN `use_count` INT NOT NULL DEFAULT -1 COMMENT '场地使用券-可用次数(-1无限制)' AFTER `venue_id`,
  ADD COLUMN `use_duration` INT DEFAULT NULL COMMENT '场地使用券-使用时长(分钟)' AFTER `use_count`;

字段说明

字段 类型 默认值 说明
receive_target TINYINT 0 0=全部, 1=仅会员, 2=仅非会员, 3=指定用户
receive_user_ids VARCHAR(1000) NULL JSON 数组如 [101,202,303], receiveTarget=3 时有值
venue_type TINYINT NULL 场地类型type=50 时填写
venue_id INT NULL 具体场地 IDtype=50 时填写
use_count INT -1 可用次数,-1=不限制
use_duration INT NULL 使用时长(分钟)

二、Java Entity — ShopCoupon.java 新增字段

在实体类中新增以下字段(与前端 model/index.ts 对齐):

// ========== 以下是新增字段 ==========

/**
 * 发放对象(0全部用户 1仅会员 2仅非会员 3指定用户)
 */
@TableField("receive_target")
private Integer receiveTarget;

/**
 * 指定用户ID列表(JSON数组格式)receiveTarget=3时使用
 * 存储格式: ["101","202","303"] 或 [101,202,303]
 */
@TableField("receive_user_ids")
private String receiveUserIds;

/**
 * 场地使用券-场地类型 (type=50时使用)
 */
@TableField("venue_type")
private Integer venueType;

/**
 * 场地使用券-指定场地ID (type=50时使用)
 */
@TableField("venue_id")
private Integer venueId;

/**
 * 场地使用券-可用次数(-1表示无限制)
 */
@TableField("use_count")
private Integer useCount;

/**
 * 场地使用券-使用时长(分钟)
 */
@TableField("use_duration")
private Integer useDuration;

注意:如果项目使用了 Lombok @Data 或手动 getter/setter确保新字段也有对应的访问方法。


三、领券/用券校验逻辑

3.1 领券接口校验(用户领取优惠券时)

在领券 Controller 或 Service 中增加校验:

/**
 * 校验用户是否有资格领取该优惠券
 *
 * @param coupon 优惠券信息
 * @param userId 当前用户ID
 * @param userGradeId 用户会员等级ID (0=非会员, >0=会员)
 * @throws BusinessException 校验不通过时抛出业务异常
 */
public void validateCouponReceiveTarget(ShopCoupon coupon, Long userId, Integer userGradeId) {
    Integer target = coupon.getReceiveTarget();

    // target=0 全部用户可领,直接通过
    if (target == null || target == 0) {
        return;
    }

    // target=1 仅会员可领
    if (target == 1) {
        if (userGradeId == null || userGradeId <= 0) {
            throw new BusinessException("该优惠券仅限会员领取");
        }
        return;
    }

    // target=2 仅非会员可领
    if (target == 2) {
        if (userGradeId != null && userGradeId > 0) {
            throw new BusinessException("该优惠券仅限非会员领取");
        }
        return;
    }

    // target=3 指定用户可领
    if (target == 3) {
        String receiveUserIds = coupon.getReceiveUserIds();
        if (StringUtils.isBlank(receiveUserIds)) {
            throw new BusinessException("该优惠券未设置指定用户");
        }
        // 解析 JSON 数组
        List<Long> allowedUserIds = JsonUtils.parseArray(receiveUserIds, Long.class);
        if (allowedUserIds == null || !allowedUserIds.contains(userId)) {
            throw new BusinessException("您不在该优惠券的发放范围内");
        }
        return;
    }

    // 未知的 target 值,默认放行(向后兼容)
}

3.2 调用位置建议

用户点击"领取优惠券"
       │
       ▼
┌──────────────────────┐
│  1. 检查优惠券是否启用   │
│  2. 检查是否已过期      │
│  3. 检查发放数量是否已完  │
│  4. 检查每人限领数量     │  ◄── 在此步骤之后、实际发券之前插入
│  ★5. 校验发放对象 ★     │     validateCouponReceiveTarget()
│  6. 创建用户优惠券记录    │
└──────────────────────┘

3.3 用券/下单时的校验(可选增强)

下单抵扣时除了常规的金额/时间/商品范围校验外,可追加:

/**
 * 下单使用优惠券时的额外校验
 */
public void validateCouponForOrder(ShopCoupon coupon, ShopUser user, OrderContext ctx) {
    // ... 已有的金额/有效期/适用范围校验 ...

    // 场地使用券(type=50)特殊校验
    if (coupon.getType() != null && coupon.getType() == 50) {
        // 验证订单是否包含场地服务
        if (!ctx.hasVenueItem()) {
            throw new BusinessException("该券仅可用于场地预订");
        }
        // 如果指定了场地类型/ID校验是否匹配
        if (coupon.getVenueType() != null && !coupon.getVenueType().equals(ctx.getVenueType())) {
            throw new BusinessException("场地类型不匹配");
        }
        if (coupon.getVenueId() != null && !coupon.getVenueId().equals(ctx.getVenueId())) {
            throw new BusinessException("指定场地不匹配");
        }
    }
}

四、JSON 工具方法说明

receive_user_ids 字段的读写需要 JSON 序列化/反序列化:

写入(保存优惠券时):

// 前端传来的是 JSON 字符串 "[101,202,303]",直接存即可
coupon.setReceiveUserIds(receiveUserIdsJsonString);

读取(校验时解析):

// 推荐使用 Jackson / Fastjson / Gson (项目已有的 JSON 库)
// 示例Jackson
ObjectMapper mapper = new ObjectMapper();
List<Long> userIds = mapper.readValue(coupon.getReceiveUserIds(),
    new TypeReference<List<Long>>() {});

五、前端-后端字段对照表

前端字段 (TypeScript) 后端字段 (Java/DB) 类型
receiveTarget receive_target Integer/TINYINT
receiveUserIds receive_user_ids String/VARCHAR
venueType venue_type Integer/TINYINT
venueId venue_id Integer/INT
useCount use_count Integer/INT
useDuration use_duration Integer/INT

六、注意事项

  1. 向后兼容receive_target 默认值 0 表示全部用户,不影响已有优惠券的领用行为
  2. JSON 格式一致性:前端提交时已序列化为 JSON 字符串,后端直接存储;读取时按 JSON 解析
  3. 场地使用券 (type=50) 是独立于普通商品券的类型,下单系统需单独适配其核销逻辑
  4. gradeId 判断会员:依赖 shop_user.grade_id 字段,grade_id > 0 为会员,=0NULL 为非会员
  5. 建议先在测试环境执行 DDL 并验证完整流程后再上生产