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 错误传递问题,确保正确注册和使用
This commit is contained in:
232
docs/coupon-backend-upgrade.md
Normal file
232
docs/coupon-backend-upgrade.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 优惠券模块后端改造方案
|
||||
|
||||
> 前端 Vue 改造已完成(paopao-vue),本文档为后端 Java 端对应的改动清单。
|
||||
|
||||
---
|
||||
|
||||
## 一、DDL — shop_coupon 表新增字段
|
||||
|
||||
```sql
|
||||
-- ============================================================
|
||||
-- 优惠券模块升级:新增发放对象控制 + 场地使用券支持
|
||||
-- 执行前请先备份表数据!
|
||||
-- ============================================================
|
||||
|
||||
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 | 具体场地 ID,type=50 时填写 |
|
||||
| `use_count` | INT | `-1` | 可用次数,-1=不限制 |
|
||||
| `use_duration` | INT | NULL | 使用时长(分钟) |
|
||||
|
||||
---
|
||||
|
||||
## 二、Java Entity — ShopCoupon.java 新增字段
|
||||
|
||||
在实体类中新增以下字段(与前端 model/index.ts 对齐):
|
||||
|
||||
```java
|
||||
// ========== 以下是新增字段 ==========
|
||||
|
||||
/**
|
||||
* 发放对象(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 中增加校验:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 校验用户是否有资格领取该优惠券
|
||||
*
|
||||
* @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 用券/下单时的校验(可选增强)
|
||||
|
||||
下单抵扣时除了常规的金额/时间/商品范围校验外,可追加:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 下单使用优惠券时的额外校验
|
||||
*/
|
||||
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 序列化/反序列化:
|
||||
|
||||
**写入(保存优惠券时):**
|
||||
```java
|
||||
// 前端传来的是 JSON 字符串 "[101,202,303]",直接存即可
|
||||
coupon.setReceiveUserIds(receiveUserIdsJsonString);
|
||||
```
|
||||
|
||||
**读取(校验时解析):**
|
||||
```java
|
||||
// 推荐使用 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` 为会员,`=0` 或 `NULL` 为非会员
|
||||
5. **建议先在测试环境执行 DDL 并验证完整流程后再上生产**
|
||||
Reference in New Issue
Block a user