Commit d2c5eaf916bd0d1f4d25fae1b20bf8b90545f9dd
1 parent
aaa01de2
优惠券:重构是否领取检查方式
Showing
8 changed files
with
127 additions
and
80 deletions
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java
| 1 | 1 | package cn.iocoder.yudao.module.promotion.controller.app.coupon; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.ObjUtil; | |
| 4 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 6 | 5 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| ... | ... | @@ -25,11 +24,9 @@ import org.springframework.web.bind.annotation.RequestParam; |
| 25 | 24 | import org.springframework.web.bind.annotation.RestController; |
| 26 | 25 | |
| 27 | 26 | import javax.annotation.Resource; |
| 28 | -import java.time.LocalDateTime; | |
| 29 | 27 | import java.util.*; |
| 30 | 28 | |
| 31 | 29 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 32 | -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | |
| 33 | 30 | import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; |
| 34 | 31 | |
| 35 | 32 | @Tag(name = "用户 App - 优惠劵模板") |
| ... | ... | @@ -46,7 +43,6 @@ public class AppCouponTemplateController { |
| 46 | 43 | @Resource |
| 47 | 44 | private ProductSpuApi productSpuApi; |
| 48 | 45 | |
| 49 | - // TODO 疯狂:这里应该还有个 list 接口哈;获得优惠劵模版列表,用于首页、商品页的优惠劵 | |
| 50 | 46 | @GetMapping("/list") |
| 51 | 47 | @Operation(summary = "获得优惠劵模版列表") |
| 52 | 48 | @Parameters({ |
| ... | ... | @@ -56,46 +52,28 @@ public class AppCouponTemplateController { |
| 56 | 52 | }) |
| 57 | 53 | public CommonResult<List<AppCouponTemplateRespVO>> getCouponTemplateList( |
| 58 | 54 | @RequestParam(value = "spuId", required = false) Long spuId, |
| 59 | - @RequestParam(value = "useType", required = false) Integer useType, | |
| 55 | + @RequestParam(value = "productScope", required = false) Integer productScope, | |
| 60 | 56 | @RequestParam(value = "count", required = false, defaultValue = "10") Integer count) { |
| 61 | - List<AppCouponTemplateRespVO> list = new ArrayList<>(); | |
| 62 | - Random random = new Random(); | |
| 63 | - for (int i = 0; i < 10; i++) { | |
| 64 | - AppCouponTemplateRespVO vo = new AppCouponTemplateRespVO(); | |
| 65 | - vo.setId(i + 1L); | |
| 66 | - vo.setName("优惠劵" + (i + 1)); | |
| 67 | - vo.setTakeLimitCount(random.nextInt(10) + 1); | |
| 68 | - vo.setUsePrice(random.nextInt(100) * 100); | |
| 69 | - vo.setValidityType(random.nextInt(2) + 1); | |
| 70 | - if (vo.getValidityType() == 1) { | |
| 71 | - vo.setValidStartTime(LocalDateTime.now().plusDays(random.nextInt(10))); | |
| 72 | - vo.setValidEndTime(LocalDateTime.now().plusDays(random.nextInt(20) + 10)); | |
| 73 | - } else { | |
| 74 | - vo.setFixedStartTerm(random.nextInt(10)); | |
| 75 | - vo.setFixedEndTerm(random.nextInt(10) + vo.getFixedStartTerm() + 1); | |
| 76 | - } | |
| 77 | - vo.setDiscountType(random.nextInt(2) + 1); | |
| 78 | - if (vo.getDiscountType() == 1) { | |
| 79 | - vo.setDiscountPercent(null); | |
| 80 | - vo.setDiscountPrice(random.nextInt(50) * 100); | |
| 81 | - vo.setDiscountLimitPrice(null); | |
| 82 | - } else { | |
| 83 | - vo.setDiscountPercent(random.nextInt(90) + 10); | |
| 84 | - vo.setDiscountPrice(null); | |
| 85 | - vo.setDiscountLimitPrice(random.nextInt(200) * 100); | |
| 86 | - } | |
| 87 | - // TODO @疯狂:是否已领取,要不在 TemplateService 搞个 static 方法,让它基于 countMap 这种去计算,这样好点? | |
| 88 | - vo.setTakeStatus(random.nextBoolean()); | |
| 89 | - list.add(vo); | |
| 90 | - } | |
| 91 | - return success(list); | |
| 57 | + // 1.1 处理查询条件:商品范围编号 | |
| 58 | + Long productScopeValue = getProductScopeValue(productScope, spuId); | |
| 59 | + // 1.2 处理查询条件:领取方式 = 直接领取 | |
| 60 | + List<Integer> canTakeTypes = Collections.singletonList(CouponTakeTypeEnum.USER.getValue()); | |
| 61 | + | |
| 62 | + // 2. 查询 | |
| 63 | + List<CouponTemplateDO> list = couponTemplateService.getCouponTemplateList(canTakeTypes, productScope, | |
| 64 | + productScopeValue, count); | |
| 65 | + | |
| 66 | + // 3.1 领取数量 | |
| 67 | + Map<Long, Boolean> canCanTakeMap = couponService.getUserCanCanTakeMap(getLoginUserId(), list); | |
| 68 | + // 3.2 拼接返回 | |
| 69 | + return success(CouponTemplateConvert.INSTANCE.convertAppList(list, canCanTakeMap)); | |
| 92 | 70 | } |
| 93 | 71 | |
| 94 | 72 | @GetMapping("/page") |
| 95 | 73 | @Operation(summary = "获得优惠劵模版分页") |
| 96 | 74 | public CommonResult<PageResult<AppCouponTemplateRespVO>> getCouponTemplatePage(AppCouponTemplatePageReqVO pageReqVO) { |
| 97 | 75 | // 1.1 处理查询条件:商品范围编号 |
| 98 | - Long productScopeValue = getProductScopeValue(pageReqVO); | |
| 76 | + Long productScopeValue = getProductScopeValue(pageReqVO.getProductScope(), pageReqVO.getSpuId()); | |
| 99 | 77 | // 1.2 处理查询条件:领取方式 = 直接领取 |
| 100 | 78 | List<Integer> canTakeTypes = Collections.singletonList(CouponTakeTypeEnum.USER.getValue()); |
| 101 | 79 | |
| ... | ... | @@ -104,35 +82,30 @@ public class AppCouponTemplateController { |
| 104 | 82 | CouponTemplateConvert.INSTANCE.convert(pageReqVO, canTakeTypes, pageReqVO.getProductScope(), productScopeValue)); |
| 105 | 83 | |
| 106 | 84 | // 3.1 领取数量 |
| 107 | - Map<Long, Integer> couponTakeCountMap = new HashMap<>(0); | |
| 108 | - Long userId = getLoginUserId(); | |
| 109 | - if (userId != null) { | |
| 110 | - List<Long> templateIds = convertList(pageResult.getList(), CouponTemplateDO::getId, | |
| 111 | - t -> ObjUtil.notEqual(t.getTakeLimitCount(), -1)); // 只查有设置“每人限领个数”的 | |
| 112 | - couponTakeCountMap = couponService.getTakeCountMapByTemplateIds(templateIds, userId); | |
| 113 | - } | |
| 85 | + Map<Long, Boolean> canCanTakeMap = couponService.getUserCanCanTakeMap(getLoginUserId(), pageResult.getList()); | |
| 114 | 86 | // 3.2 拼接返回 |
| 115 | - return success(CouponTemplateConvert.INSTANCE.convertAppPage(pageResult, couponTakeCountMap)); | |
| 87 | + return success(CouponTemplateConvert.INSTANCE.convertAppPage(pageResult, canCanTakeMap)); | |
| 116 | 88 | } |
| 117 | 89 | |
| 118 | 90 | /** |
| 119 | - * 获得分页查询的商品范围 | |
| 91 | + * 获得商品的使用范围编号 | |
| 120 | 92 | * |
| 121 | - * @param pageReqVO 分页查询 | |
| 122 | - * @return 商品范围 | |
| 93 | + * @param productScope 商品范围 | |
| 94 | + * @param spuId 商品 SPU 编号 | |
| 95 | + * @return 商品范围编号 | |
| 123 | 96 | */ |
| 124 | - private Long getProductScopeValue(AppCouponTemplatePageReqVO pageReqVO) { | |
| 125 | - // 通用券:清除商品范围 | |
| 126 | - if (pageReqVO.getProductScope() == null || ObjectUtils.equalsAny(pageReqVO.getProductScope(), PromotionProductScopeEnum.ALL.getScope(), null)) { | |
| 97 | + private Long getProductScopeValue(Integer productScope, Long spuId) { | |
| 98 | + // 通用券:没有商品范围 | |
| 99 | + if (productScope == null || ObjectUtils.equalsAny(productScope, PromotionProductScopeEnum.ALL.getScope(), null)) { | |
| 127 | 100 | return null; |
| 128 | 101 | } |
| 129 | - // 品类券:查询商品的品类 | |
| 130 | - if (Objects.equals(pageReqVO.getProductScope(), PromotionProductScopeEnum.CATEGORY.getScope()) && pageReqVO.getSpuId() != null) { | |
| 131 | - return Optional.ofNullable(productSpuApi.getSpu(pageReqVO.getSpuId())) | |
| 102 | + // 品类券:查询商品的品类编号 | |
| 103 | + if (Objects.equals(productScope, PromotionProductScopeEnum.CATEGORY.getScope()) && spuId != null) { | |
| 104 | + return Optional.ofNullable(productSpuApi.getSpu(spuId)) | |
| 132 | 105 | .map(ProductSpuRespDTO::getCategoryId).orElse(null); |
| 133 | 106 | } |
| 134 | 107 | // 商品卷:直接返回 |
| 135 | - return pageReqVO.getSpuId(); | |
| 108 | + return spuId; | |
| 136 | 109 | } |
| 137 | 110 | |
| 138 | 111 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java
| ... | ... | @@ -55,7 +55,7 @@ public class AppCouponTemplateRespVO { |
| 55 | 55 | |
| 56 | 56 | // ========== 用户相关字段 ========== |
| 57 | 57 | |
| 58 | - @Schema(description = "是否已领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | |
| 59 | - private Boolean takeStatus; | |
| 58 | + @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | |
| 59 | + private Boolean canTake; | |
| 60 | 60 | |
| 61 | 61 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponTemplateConvert.java
| ... | ... | @@ -37,21 +37,25 @@ public interface CouponTemplateConvert { |
| 37 | 37 | |
| 38 | 38 | PageResult<AppCouponTemplateRespVO> convertAppPage(PageResult<CouponTemplateDO> pageResult); |
| 39 | 39 | |
| 40 | - default PageResult<AppCouponTemplateRespVO> convertAppPage(PageResult<CouponTemplateDO> pageResult, Map<Long, Integer> couponTakeCountMap) { | |
| 40 | + List<AppCouponTemplateRespVO> convertAppList(List<CouponTemplateDO> list); | |
| 41 | + | |
| 42 | + default PageResult<AppCouponTemplateRespVO> convertAppPage(PageResult<CouponTemplateDO> pageResult, Map<Long, Boolean> userCanTakeMap) { | |
| 41 | 43 | PageResult<AppCouponTemplateRespVO> result = convertAppPage(pageResult); |
| 42 | - if (MapUtil.isEmpty(couponTakeCountMap)) { | |
| 43 | - return result; | |
| 44 | - } | |
| 45 | - for (AppCouponTemplateRespVO template : result.getList()) { | |
| 46 | - // 每人领取数量无限制 | |
| 47 | - if (template.getTakeLimitCount() == -1) { | |
| 48 | - template.setTakeStatus(false); | |
| 49 | - continue; | |
| 50 | - } | |
| 44 | + copyTo(result.getList(), userCanTakeMap); | |
| 45 | + return result; | |
| 46 | + } | |
| 47 | + | |
| 48 | + default List<AppCouponTemplateRespVO> convertAppList(List<CouponTemplateDO> list, Map<Long, Boolean> userCanTakeMap) { | |
| 49 | + List<AppCouponTemplateRespVO> result = convertAppList(list); | |
| 50 | + copyTo(result, userCanTakeMap); | |
| 51 | + return result; | |
| 52 | + } | |
| 53 | + | |
| 54 | + default void copyTo(List<AppCouponTemplateRespVO> list, Map<Long, Boolean> userCanTakeMap) { | |
| 55 | + for (AppCouponTemplateRespVO template : list) { | |
| 51 | 56 | // 检查已领取数量是否超过限领数量 |
| 52 | - template.setTakeStatus(MapUtil.getInt(couponTakeCountMap, template.getId(), 0) >= template.getTakeLimitCount()); | |
| 57 | + template.setCanTake(MapUtil.getBool(userCanTakeMap, template.getId(), false)); | |
| 53 | 58 | } |
| 54 | - return result; | |
| 55 | 59 | } |
| 56 | 60 | |
| 57 | 61 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
| ... | ... | @@ -24,16 +24,8 @@ import java.util.function.Consumer; |
| 24 | 24 | public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> { |
| 25 | 25 | |
| 26 | 26 | default PageResult<CouponTemplateDO> selectPage(CouponTemplatePageReqVO reqVO) { |
| 27 | - // 构建可领取的查询条件, 好啰嗦 ( ╯-_-)╯┴—┴ | |
| 28 | - Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = null; | |
| 29 | - if (CollUtil.isNotEmpty(reqVO.getCanTakeTypes())) { | |
| 30 | - canTakeConsumer = w -> | |
| 31 | - w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的 | |
| 32 | - .in(CouponTemplateDO::getTakeType, reqVO.getCanTakeTypes()) // 2. 领取方式一致 | |
| 33 | - .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期 | |
| 34 | - .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now())) | |
| 35 | - .apply(" take_count < total_count "); // 4. 剩余数量大于 0 | |
| 36 | - } | |
| 27 | + // 构建可领取的查询条件 | |
| 28 | + Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(reqVO.getCanTakeTypes()); | |
| 37 | 29 | // 执行分页查询 |
| 38 | 30 | return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>() |
| 39 | 31 | .likeIfPresent(CouponTemplateDO::getName, reqVO.getName()) |
| ... | ... | @@ -52,4 +44,30 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> { |
| 52 | 44 | default List<CouponTemplateDO> selectListByTakeType(Integer takeType) { |
| 53 | 45 | return selectList(CouponTemplateDO::getTakeType, takeType); |
| 54 | 46 | } |
| 47 | + | |
| 48 | + default List<CouponTemplateDO> selectList(List<Integer> canTakeTypes, Integer productScope, Long productScopeValue, Integer count) { | |
| 49 | + // 构建可领取的查询条件 | |
| 50 | + Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes); | |
| 51 | + return selectList(new LambdaQueryWrapperX<CouponTemplateDO>() | |
| 52 | + .eqIfPresent(CouponTemplateDO::getProductScope, productScope) | |
| 53 | + .and(productScopeValue != null, w -> w.apply("FIND_IN_SET({0}, product_scope_values)", | |
| 54 | + productScopeValue)) | |
| 55 | + .and(canTakeConsumer != null, canTakeConsumer) | |
| 56 | + .last(" LIMIT " + count) | |
| 57 | + .orderByDesc(CouponTemplateDO::getId)); | |
| 58 | + } | |
| 59 | + | |
| 60 | + static Consumer<LambdaQueryWrapper<CouponTemplateDO>> buildCanTakeQueryConsumer(List<Integer> canTakeTypes) { | |
| 61 | + Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = null; | |
| 62 | + if (CollUtil.isNotEmpty(canTakeTypes)) { | |
| 63 | + canTakeConsumer = w -> | |
| 64 | + w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的 | |
| 65 | + .in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致 | |
| 66 | + .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期 | |
| 67 | + .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now())) | |
| 68 | + .apply(" take_count < total_count "); // 4. 剩余数量大于 0 | |
| 69 | + } | |
| 70 | + return canTakeConsumer; | |
| 71 | + } | |
| 72 | + | |
| 55 | 73 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
| ... | ... | @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 5 | import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; |
| 6 | 6 | import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO; |
| 7 | 7 | import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; |
| 8 | +import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; | |
| 8 | 9 | import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; |
| 9 | 10 | import cn.iocoder.yudao.module.promotion.service.coupon.bo.CouponTakeCountBO; |
| 10 | 11 | |
| ... | ... | @@ -175,4 +176,13 @@ public interface CouponService { |
| 175 | 176 | */ |
| 176 | 177 | int expireCoupon(); |
| 177 | 178 | |
| 179 | + /** | |
| 180 | + * 获取用户是否可以领取优惠券 | |
| 181 | + * | |
| 182 | + * @param userId 用户编号 | |
| 183 | + * @param templates 优惠券列表 | |
| 184 | + * @return 是否可以领取 | |
| 185 | + */ | |
| 186 | + Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates); | |
| 187 | + | |
| 178 | 188 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
| ... | ... | @@ -36,7 +36,7 @@ import java.util.Set; |
| 36 | 36 | import java.util.stream.Collectors; |
| 37 | 37 | |
| 38 | 38 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 39 | -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | |
| 39 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; | |
| 40 | 40 | import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; |
| 41 | 41 | import static java.util.Arrays.asList; |
| 42 | 42 | |
| ... | ... | @@ -229,6 +229,29 @@ public class CouponServiceImpl implements CouponService { |
| 229 | 229 | return count; |
| 230 | 230 | } |
| 231 | 231 | |
| 232 | + @Override | |
| 233 | + public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) { | |
| 234 | + Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true); | |
| 235 | + // 未登录时,都显示可以领取 | |
| 236 | + if (userId == null) { | |
| 237 | + return userCanTakeMap; | |
| 238 | + } | |
| 239 | + | |
| 240 | + // 过滤领取数量无限制的 | |
| 241 | + Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1); | |
| 242 | + | |
| 243 | + // 检查用户领取的数量是否超过限制 | |
| 244 | + if (CollUtil.isNotEmpty(templateIds)) { | |
| 245 | + Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId); | |
| 246 | + for (CouponTemplateDO template : templates) { | |
| 247 | + Integer takeCount = couponTakeCountMap.get(template.getId()); | |
| 248 | + userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount()); | |
| 249 | + } | |
| 250 | + } | |
| 251 | + | |
| 252 | + return userCanTakeMap; | |
| 253 | + } | |
| 254 | + | |
| 232 | 255 | /** |
| 233 | 256 | * 过期单个优惠劵 |
| 234 | 257 | * | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
| ... | ... | @@ -78,4 +78,17 @@ public interface CouponTemplateService { |
| 78 | 78 | * @return 优惠券模板列表 |
| 79 | 79 | */ |
| 80 | 80 | List<CouponTemplateDO> getCouponTemplateByTakeType(CouponTakeTypeEnum takeType); |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 获得优惠券模板列表 | |
| 84 | + * | |
| 85 | + * @param canTakeTypes 可领取的类型列表 | |
| 86 | + * @param productScope 商品使用范围类型 | |
| 87 | + * @param productScopeValue 商品使用范围编号 | |
| 88 | + * @param count 查询数量 | |
| 89 | + * @return 优惠券模板列表 | |
| 90 | + */ | |
| 91 | + List<CouponTemplateDO> getCouponTemplateList(List<Integer> canTakeTypes, Integer productScope, | |
| 92 | + Long productScopeValue, Integer count); | |
| 93 | + | |
| 81 | 94 | } | ... | ... |
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
| ... | ... | @@ -120,4 +120,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { |
| 120 | 120 | return couponTemplateMapper.selectListByTakeType(takeType.getValue()); |
| 121 | 121 | } |
| 122 | 122 | |
| 123 | + @Override | |
| 124 | + public List<CouponTemplateDO> getCouponTemplateList(List<Integer> canTakeTypes, Integer productScope, | |
| 125 | + Long productScopeValue, Integer count) { | |
| 126 | + return couponTemplateMapper.selectList(canTakeTypes, productScope, productScopeValue, count); | |
| 127 | + } | |
| 128 | + | |
| 123 | 129 | } | ... | ... |