|
|
@@ -0,0 +1,392 @@
|
|
|
+package com.ytpm.novel.controller;
|
|
|
+
|
|
|
+
|
|
|
+import cn.hutool.core.bean.BeanUtil;
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.github.pagehelper.PageHelper;
|
|
|
+import com.github.pagehelper.PageInfo;
|
|
|
+import com.ytpm.advertise.enums.AdSourceTypeEnum;
|
|
|
+import com.ytpm.agent.enums.UserStatusEnum;
|
|
|
+import com.ytpm.agent.param.AdRecordListParam;
|
|
|
+import com.ytpm.agent.param.AuditCheckParam;
|
|
|
+import com.ytpm.agent.param.AuditUserParam;
|
|
|
+import com.ytpm.agent.view.AgentAuditCheckVO;
|
|
|
+import com.ytpm.app.model.YtDyzAdRecord;
|
|
|
+import com.ytpm.app.model.YtDyzLoginRecord;
|
|
|
+import com.ytpm.app.model.YtNovelUser;
|
|
|
+import com.ytpm.app.model.YtUser;
|
|
|
+import com.ytpm.app.param.AppQueryUserTodayTimeParam;
|
|
|
+import com.ytpm.app.param.AppUserParam;
|
|
|
+import com.ytpm.app.param.AppUserQueryParam;
|
|
|
+import com.ytpm.app.param.AppUserTodayBannedParam;
|
|
|
+import com.ytpm.app.param.YtAppUserListParam;
|
|
|
+import com.ytpm.app.view.HourCountView;
|
|
|
+import com.ytpm.app.view.YtAppUserListView;
|
|
|
+import com.ytpm.general.RepMessage;
|
|
|
+import com.ytpm.general.Result;
|
|
|
+import com.ytpm.general.ResultTable;
|
|
|
+import com.ytpm.novel.dao.AdRecordMapper;
|
|
|
+import com.ytpm.novel.dao.AppUserMapper;
|
|
|
+import com.ytpm.novel.dao.LoginRecordMapper;
|
|
|
+import com.ytpm.novel.dao.NovelAdRecordMapper;
|
|
|
+import com.ytpm.novel.model.dto.UserStaticParam;
|
|
|
+import com.ytpm.novel.redis.RedisService;
|
|
|
+import com.ytpm.risk.enums.BannedTypeEnum;
|
|
|
+import com.ytpm.risk.view.RiskConfigView;
|
|
|
+import com.ytpm.risk.view.RiskTemplateView;
|
|
|
+import io.swagger.annotations.ApiOperation;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+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 javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Calendar;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author lih
|
|
|
+ * @date 2025-10-09 14:24
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequestMapping("/user")
|
|
|
+public class UserController {
|
|
|
+ @Resource
|
|
|
+ private AppUserMapper appUserMapper;
|
|
|
+ @Resource
|
|
|
+ private LoginRecordMapper loginRecordMapper;
|
|
|
+ @Resource
|
|
|
+ private AdRecordMapper adRecordMapper;
|
|
|
+ @Resource
|
|
|
+ private NovelAdRecordMapper novelAdRecordMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RedisService redisService;
|
|
|
+
|
|
|
+
|
|
|
+ @ApiOperation("根据应用ID查询当月注册用户")
|
|
|
+ @GetMapping("/getMonthRegistryUser")
|
|
|
+ public List<YtNovelUser> getMonthRegistryUser(@RequestParam("appIds") String appIds, @RequestParam("type") Integer type) {
|
|
|
+ return appUserMapper.getMonthRegistryUser(appIds, type);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询app 所有玩家
|
|
|
+ * 汇聚分库应用
|
|
|
+ */
|
|
|
+ @PostMapping("/queryAll")
|
|
|
+ public ResultTable<YtAppUserListView> queryAll(@RequestBody YtAppUserListParam param) {
|
|
|
+ //查询主应用
|
|
|
+ PageHelper.startPage(param.getPage(), param.getLimit());
|
|
|
+ List<YtAppUserListView> result = appUserMapper.queryAll(param);
|
|
|
+ String collect = result.stream().map(YtAppUserListView::getUserId).collect(Collectors.joining(","));
|
|
|
+ List<YtDyzAdRecord> adRecords = adRecordMapper.getByUserIds(collect);
|
|
|
+ List<YtDyzLoginRecord> dyzLogins = loginRecordMapper.getLoginRecordByIds(collect);
|
|
|
+ Map<String, List<YtDyzAdRecord>> userAdMap = adRecords.stream().collect(
|
|
|
+ Collectors.groupingBy(YtDyzAdRecord::getUserId));
|
|
|
+ Map<String, List<YtDyzLoginRecord>> loginMap = dyzLogins.stream().collect(
|
|
|
+ Collectors.groupingBy(YtDyzLoginRecord::getUserId));
|
|
|
+ setUserExtInfo(result, userAdMap, loginMap);
|
|
|
+ return ResultTable.resultTableOk(new PageInfo<>(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置视频记录和登录信息
|
|
|
+ */
|
|
|
+ private void setUserExtInfo(List<YtAppUserListView> result, Map<String, List<YtDyzAdRecord>> userAdMap,
|
|
|
+ Map<String, List<YtDyzLoginRecord>> loginMap) {
|
|
|
+ for (YtAppUserListView user : result) {
|
|
|
+ if (loginMap.containsKey(user.getUserId())) {
|
|
|
+ user.setLoginRecordList(loginMap.get(user.getUserId()));
|
|
|
+ }
|
|
|
+ //获取今日收益 | 今日视频播放数
|
|
|
+ if (userAdMap.containsKey(user.getUserId())) {
|
|
|
+ List<YtDyzAdRecord> ytDyzAdRecords = userAdMap.get(user.getUserId());
|
|
|
+ List<YtDyzAdRecord> collect = ytDyzAdRecords.stream().filter(
|
|
|
+ s -> DateUtil.isSameDay(DateUtil.date(), DateUtil.parseDate(s.getFinishTime()))).collect(Collectors.toList());
|
|
|
+ BigDecimal todayIncome = collect.stream().map(YtDyzAdRecord::getRevenue).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
+ user.setTodayVideo((int) collect.stream().filter(
|
|
|
+ s -> s.getAdSourceType() == AdSourceTypeEnum.rewarded_video.getAdSourceType()).count());
|
|
|
+ user.setTodayIncome(todayIncome);
|
|
|
+ // 统计4天-今日收益=前三日收益
|
|
|
+ if (user.getNearlyIncome() != null && user.getNearlyIncome().compareTo(todayIncome) >= 0) {
|
|
|
+ user.setNearlyIncome(user.getNearlyIncome().subtract(todayIncome));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ user.setTodayIncome(BigDecimal.ZERO);
|
|
|
+ user.setTodayVideo(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("根据时间统计收益")
|
|
|
+ @PostMapping("/getRevenueByTime")
|
|
|
+ public BigDecimal getRevenueByTime(@RequestBody YtAppUserListParam param) {
|
|
|
+ BigDecimal result = new BigDecimal(0);
|
|
|
+ BigDecimal dyzRevenue = adRecordMapper.getRevenueByTime(param);
|
|
|
+ if (Objects.nonNull(dyzRevenue)) {
|
|
|
+ result = result.add(dyzRevenue);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 主键查询用户信息
|
|
|
+ */
|
|
|
+ @GetMapping("/getUserInfo")
|
|
|
+ public Result<YtNovelUser> getUserInfo(@RequestParam("userId") String userId) {
|
|
|
+ YtNovelUser user = appUserMapper.selectPrimaryKey(userId);
|
|
|
+ user.setLoginRecordList(loginRecordMapper.getLoginRecords(userId));
|
|
|
+ user.setNovelAdRecordList(novelAdRecordMapper.selectToDayNovelRecode(userId));
|
|
|
+ return Result.resultObjOk(user);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询用户信息
|
|
|
+ */
|
|
|
+ @PostMapping("/getUserList")
|
|
|
+ public ResultTable<YtNovelUser> getUserList(@RequestBody AppUserParam param) {
|
|
|
+ List<YtNovelUser> userList = appUserMapper.getUserList(param);
|
|
|
+ if (CollUtil.isEmpty(userList)) {
|
|
|
+ userList = new ArrayList<>();
|
|
|
+ }
|
|
|
+ return ResultTable.resultTableOk(new PageInfo<>(userList));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/getUserAll")
|
|
|
+ public ResultTable<YtNovelUser> getUserAll(@RequestBody AppUserParam param) {
|
|
|
+ List<YtNovelUser> userList = appUserMapper.getUserList(param);
|
|
|
+ return ResultTable.resultTableOk(new PageInfo<>(userList));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改用户信息
|
|
|
+ */
|
|
|
+ @PostMapping("/updateUserInfo")
|
|
|
+ public Result<?> updateUserInfo(@RequestBody YtUser ytUser) {
|
|
|
+ YtNovelUser novelUser = new YtNovelUser();
|
|
|
+ BeanUtil.copyProperties(ytUser, novelUser);
|
|
|
+ appUserMapper.updateUser(novelUser);
|
|
|
+ return Result.resultOk(RepMessage.MODIFY_SUCCESS);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询用户的广告记录
|
|
|
+ */
|
|
|
+ @GetMapping("/adRecords")
|
|
|
+ public ResultTable<YtDyzAdRecord> adRecords(@RequestParam(name = "userId", required = true) String userId,
|
|
|
+ @RequestParam(name = "adsourceType", required = false) Integer adsourceType) {
|
|
|
+ return ResultTable.resultTableOk(new PageInfo<>(adRecordMapper.getByUserId(userId, adsourceType)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/adRecords/page")
|
|
|
+ public ResultTable<YtDyzAdRecord> adRecordsPage(@RequestBody AdRecordListParam param) {
|
|
|
+ PageHelper.startPage(param.getPage(), param.getLimit());
|
|
|
+ List<YtDyzAdRecord> adrecords = adRecordMapper.getByUserByParam(param);
|
|
|
+ return ResultTable.resultTableOk(new PageInfo<>(adrecords));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询广告记录统计数
|
|
|
+ */
|
|
|
+ @GetMapping("/adRecords/count/month")
|
|
|
+ public Result<Integer> queryRecordMonthCount(@RequestParam(name = "userId") String userId,
|
|
|
+ @RequestParam(name = "adSourceType", required = false) Integer adSourceType,
|
|
|
+ @RequestParam(name = "startTime", required = false) String startTime) {
|
|
|
+ Date time;
|
|
|
+ // 统计起始时间为空,则默认查询当月信息
|
|
|
+ if (StrUtil.isEmpty(startTime)) {
|
|
|
+ Calendar calendar = Calendar.getInstance();
|
|
|
+ calendar.setTime(new Date());
|
|
|
+ calendar.set(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
+ calendar.set(Calendar.MINUTE, 0);
|
|
|
+ calendar.set(Calendar.SECOND, 0);
|
|
|
+ time = calendar.getTime();
|
|
|
+ } else {
|
|
|
+ time = DateUtil.parse(startTime, "yyyy-MM-dd HH:mm:ss");
|
|
|
+ }
|
|
|
+ return Result.resultObjOk(adRecordMapper.getRecordMonthCount(userId, adSourceType, time));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/queryUserByTime")
|
|
|
+ public List<YtNovelUser> queryUserByTime(@RequestBody AppUserQueryParam appUserQueryParam) {
|
|
|
+ UserStaticParam param = new UserStaticParam(appUserQueryParam);
|
|
|
+ return appUserMapper.queryAllByTime(param);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/queryUserByTodayTime")
|
|
|
+ public int[] queryUserByTodayTime(@RequestBody AppQueryUserTodayTimeParam appQueryUserTodayTimeParam) {
|
|
|
+ // 获取当前日期
|
|
|
+ LocalDate localDate = LocalDate.now();
|
|
|
+
|
|
|
+ // 构建当天起始时间段
|
|
|
+ LocalDateTime startOfDay = localDate.atStartOfDay();
|
|
|
+ LocalDateTime endOfDay = startOfDay.plusDays(1).minusSeconds(1);
|
|
|
+
|
|
|
+ Date startTime = Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
+ Date endTime = Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
+ UserStaticParam param = new UserStaticParam();
|
|
|
+ param.setStartTime(startTime);
|
|
|
+ param.setEndTime(endTime);
|
|
|
+ param.setAppIdList(appQueryUserTodayTimeParam.getAppIdList());
|
|
|
+ // 查询用户注册数据
|
|
|
+ List<HourCountView> hourCountViews = appUserMapper.countUsersByHour(param);
|
|
|
+ // 初始化每小时用户数量数组
|
|
|
+ int[] hourlyCounts = new int[24];
|
|
|
+ for (HourCountView hc : hourCountViews) {
|
|
|
+ if (hc.getHour() >= 0 && hc.getHour() < 24) {
|
|
|
+ hourlyCounts[hc.getHour()] = hc.getCount();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return hourlyCounts;
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/queryLoginRecords")
|
|
|
+ public List<String> queryLoginRecords(@RequestBody AppUserQueryParam appUserQueryParam) {
|
|
|
+ return loginRecordMapper.queryLoginCount(appUserQueryParam);
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/queryByOpenid")
|
|
|
+ public List<YtNovelUser> queryByOpenid(@RequestParam("openid") String openid) {
|
|
|
+ return appUserMapper.queryByOpenid(openid);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/queryTodayBanned")
|
|
|
+ public List<YtNovelUser> queryTodayBanned(@RequestBody AppUserTodayBannedParam appUserTodayBannedParam) {
|
|
|
+ UserStaticParam param = new UserStaticParam(appUserTodayBannedParam);
|
|
|
+ return appUserMapper.queryTodayBanned(param);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/unLockUser")
|
|
|
+ public void unLockUser(@RequestParam("userIds") String userIds) {
|
|
|
+ List<YtNovelUser> users = appUserMapper.queryByUserIds(userIds);
|
|
|
+ if (CollUtil.isNotEmpty(users)) {
|
|
|
+ appUserMapper.unlockUser(userIds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/lockUser")
|
|
|
+ public YtNovelUser lockUser(@RequestParam("userId") String userId, @RequestParam("userStatus") Integer userStatus) {
|
|
|
+ YtNovelUser dyzUser = appUserMapper.selectPrimaryKey(userId);
|
|
|
+ YtNovelUser newUser = new YtNovelUser();
|
|
|
+ newUser.setUserId(userId);
|
|
|
+ newUser.setUserStatus(userStatus);
|
|
|
+ newUser.setRiskReason(BannedTypeEnum.CHANNEL.getDesc() + "禁止登录");
|
|
|
+ if (Objects.nonNull(dyzUser)) {
|
|
|
+ appUserMapper.updateUser(newUser);
|
|
|
+ return dyzUser;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量审核用户是否满足风控规则
|
|
|
+ * 不满足风控规则的审核通过后直接封禁指定天数
|
|
|
+ */
|
|
|
+ @PostMapping("/batchAudit")
|
|
|
+ public void batchAudit(@RequestBody AuditCheckParam checkParam) {
|
|
|
+ AuditUserParam auditParam = checkParam.getAuditParam();
|
|
|
+ RiskTemplateView ecpmLimit = checkParam.getEcpmLimit();
|
|
|
+ RiskTemplateView revenueLimit = checkParam.getRevenueLimit();
|
|
|
+ //查询用户,指定应用激励视频的广告记录
|
|
|
+ List<AgentAuditCheckVO> auditList = new ArrayList<>();
|
|
|
+ List<AgentAuditCheckVO> auditCheckList = appUserMapper.queryTodayUserAd(auditParam);
|
|
|
+ if (CollUtil.isNotEmpty(auditCheckList)) {
|
|
|
+ auditList.addAll(auditCheckList);
|
|
|
+ }
|
|
|
+ Map<String, List<YtDyzAdRecord>> adRecordMap = auditList.stream().collect(
|
|
|
+ Collectors.toMap(AgentAuditCheckVO::getUserId, AgentAuditCheckVO::getAdRecordList));
|
|
|
+ checkRisk742(ecpmLimit, adRecordMap, auditParam);
|
|
|
+ checkRisk746(revenueLimit, adRecordMap, auditParam);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验默认风控规则742
|
|
|
+ */
|
|
|
+ private void checkRisk742(RiskTemplateView ecpmLimit, Map<String, List<YtDyzAdRecord>> adRecordMap, AuditUserParam auditParam) {
|
|
|
+ if (ecpmLimit.getEnabled() != 1) return;
|
|
|
+ Map<String, String> limitMap = ecpmLimit.getConfigList().stream().collect(
|
|
|
+ Collectors.toMap(RiskConfigView::getFieldName, RiskConfigView::getConfigVal));
|
|
|
+ int adCount = 0;
|
|
|
+ for (Map.Entry<String, List<YtDyzAdRecord>> entry : adRecordMap.entrySet()) {
|
|
|
+ adCount = CollUtil.isEmpty(entry.getValue()) ? adCount : entry.getValue().size();
|
|
|
+ ++adCount;
|
|
|
+ //判断当日观看视频数已经达到风控条件预设的视频数
|
|
|
+ int firstAdCount = Integer.parseInt(limitMap.get("firstAdCount"));
|
|
|
+ if (adCount >= firstAdCount) {
|
|
|
+ //过滤出ecpm值小于预设值的视频数
|
|
|
+ int ecpm = Integer.parseInt(limitMap.get("ecpm"));
|
|
|
+ long count = entry.getValue().stream().filter(
|
|
|
+ s -> Objects.nonNull(s.getEcpm()) && (s.getEcpm().compareTo(BigDecimal.valueOf(ecpm))) < 0).count();
|
|
|
+ int exact = Math.toIntExact(count);
|
|
|
+ //判断小于预设值的视频数超出预设次数
|
|
|
+ int haveCount = Integer.parseInt(limitMap.get("haveCount"));
|
|
|
+ if (exact > haveCount) {//把风控不满足的用户记录redis 进行封禁
|
|
|
+ checkLock(entry, auditParam);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用户锁定操作
|
|
|
+ */
|
|
|
+ private void checkLock(Map.Entry<String, List<YtDyzAdRecord>> entry, AuditUserParam auditParam) {
|
|
|
+ log.error("执行对低价值用户{}的定时风控操作", entry.getKey());
|
|
|
+ if (auditParam.getEffectTime() == 0) { //立即锁定
|
|
|
+ lockUser(entry.getKey(), UserStatusEnum.RISK.getCode());
|
|
|
+ redisService.setTimeOutHoursStr("unlock_" + entry.getKey(), auditParam.getAppId(), auditParam.getBannedLimit() * 24L);
|
|
|
+ } else {//延迟锁定
|
|
|
+ redisService.setTimeOutHoursStr("lock_" + entry.getKey(), auditParam.getAppId(), auditParam.getEffectTime());
|
|
|
+ redisService.setTimeOutHoursStr("unlock_" + entry.getKey(), auditParam.getAppId(),
|
|
|
+ auditParam.getBannedLimit() * 24L + auditParam.getEffectTime());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验默认风控规则746
|
|
|
+ */
|
|
|
+ private void checkRisk746(RiskTemplateView revenueLimit, Map<String, List<YtDyzAdRecord>> adRecordMap,
|
|
|
+ AuditUserParam auditParam) {
|
|
|
+ if (revenueLimit.getEnabled() != 1) return;
|
|
|
+ Map<String, String> revenueMap = revenueLimit.getConfigList().stream().collect(
|
|
|
+ Collectors.toMap(RiskConfigView::getFieldName, RiskConfigView::getConfigVal));
|
|
|
+ for (Map.Entry<String, List<YtDyzAdRecord>> entry : adRecordMap.entrySet()) {
|
|
|
+ List<YtDyzAdRecord> revenues = entry.getValue().stream().filter(
|
|
|
+ s -> Objects.nonNull(s.getRevenue()) && (s.getRevenue().compareTo(BigDecimal.ZERO)) > 0)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ int incomeCount = 0;
|
|
|
+ incomeCount = CollUtil.isEmpty(revenues) ? incomeCount : entry.getValue().size();
|
|
|
+ //判断当日获得收益的广告达到预设数值,触发风控规则
|
|
|
+ int rewardCount = Integer.parseInt(revenueMap.get("rewardCount"));
|
|
|
+ if (incomeCount >= rewardCount) {
|
|
|
+ BigDecimal income = new BigDecimal(revenueMap.get("income"));
|
|
|
+ //获取最先的两条
|
|
|
+ BigDecimal reduce = revenues.stream().map(YtDyzAdRecord::getRevenue).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
+ if (reduce.compareTo(income) < 0) {
|
|
|
+ checkLock(entry, auditParam);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|