src/main/java/com/webmanage/controller/PointsController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/dto/DeductUserPointsDTO.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/entity/PointsTransaction.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/mapper/PointsTransactionMapper.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/service/PointsFlowService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/service/impl/PointsFlowServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
target/classes/application-dev.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
target/classes/application-prod.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
target/classes/application-test.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
target/classes/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/webmanage/controller/PointsController.java
@@ -272,6 +272,24 @@ } } // ==================== 积分扣减 ==================== @PostMapping("/user/deduct") @ApiOperation("扣减用户积分") public Result<Object> deductUserPoints(@Valid @RequestBody DeductUserPointsDTO deductDTO) { try { boolean result = pointsFlowService.deductUserPoints(deductDTO); if (result) { return Result.success("积分扣减成功"); } else { return Result.error("积分扣减失败"); } } catch (Exception e) { log.error("积分扣减失败", e); return Result.error("积分扣减失败:" + e.getMessage()); } } // ==================== 积分流水数据类目 ==================== @GetMapping("/flow/categories") src/main/java/com/webmanage/dto/DeductUserPointsDTO.java
New file @@ -0,0 +1,41 @@ package com.webmanage.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; /** * 扣减用户积分DTO */ @Data @ApiModel(value = "DeductUserPointsDTO", description = "扣减用户积分") public class DeductUserPointsDTO { @ApiModelProperty("用户ID") @NotBlank(message = "用户ID不能为空") private String userId; @ApiModelProperty("单位ID") private String unitId; @ApiModelProperty("扣减积分数量") @NotNull(message = "扣减积分数量不能为空") @Positive(message = "扣减积分数量必须大于0") private Integer points; @ApiModelProperty("订单ID") private String orderId; @ApiModelProperty("扣减原因/备注") private String remark; @ApiModelProperty("数据类目") private String dataCategory = "resource_transaction"; @ApiModelProperty("数据类型") private Integer dataType = 1; // 1表示消耗 } src/main/java/com/webmanage/entity/PointsTransaction.java
New file @@ -0,0 +1,74 @@ package com.webmanage.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDateTime; /** * 积分交易记录实体,对应表 tb_points_transaction */ @Data @TableName("tb_points_transaction") @ApiModel(value = "PointsTransaction", description = "积分交易记录") public class PointsTransaction { @ApiModelProperty("主键ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty("数据类目") @TableField("data_category") private String dataCategory; @ApiModelProperty("名称") @TableField("transaction_name") private String transactionName; @ApiModelProperty("时间") @TableField("transaction_time") private LocalDateTime transactionTime; @ApiModelProperty("积分变动值") @TableField("points_change") private Integer pointsChange; @ApiModelProperty("积分规则类型:获取/消耗") @TableField("rule_type") private String ruleType; @ApiModelProperty("用户ID") @TableField("user_id") private Long userId; @ApiModelProperty("企业ID") @TableField("unit_id") private Long unitId; @ApiModelProperty("用户类型:个人用户/单位用户") @TableField("user_type") private String userType; @ApiModelProperty("关联规则ID") @TableField("rule_id") private Long ruleId; @ApiModelProperty("关联规则详情ID") @TableField("detail_id") private Long detailId; @ApiModelProperty("创建时间") @TableField("created_at") private LocalDateTime createdAt; @ApiModelProperty("逻辑删除") @TableField("deleted") private Integer deleted; } src/main/java/com/webmanage/mapper/PointsTransactionMapper.java
New file @@ -0,0 +1,11 @@ package com.webmanage.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.webmanage.entity.PointsTransaction; import org.apache.ibatis.annotations.Mapper; @Mapper public interface PointsTransactionMapper extends BaseMapper<PointsTransaction> { } src/main/java/com/webmanage/service/PointsFlowService.java
@@ -3,6 +3,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.webmanage.common.PageResult; import com.webmanage.dto.AddPointsFlowDTO; import com.webmanage.dto.DeductUserPointsDTO; import com.webmanage.dto.PointsFlowQueryDTO; import com.webmanage.entity.PointsFlow; import com.webmanage.entity.UserPoints; @@ -41,6 +42,11 @@ boolean addPointsFlowByRule(AddPointsFlowDTO addPointsFlowDTO); /** * 扣减用户积分 */ boolean deductUserPoints(DeductUserPointsDTO deductDTO); /** * 获取用户积分统计 */ UserPoints getUserPointsTotal(String userId); src/main/java/com/webmanage/service/impl/PointsFlowServiceImpl.java
@@ -7,6 +7,7 @@ import com.webmanage.common.BusinessException; import com.webmanage.common.PageResult; import com.webmanage.dto.AddPointsFlowDTO; import com.webmanage.dto.DeductUserPointsDTO; import com.webmanage.dto.PointsFlowQueryDTO; import com.webmanage.emun.RuleTypeEnum; import com.webmanage.entity.PointsFlow; @@ -14,6 +15,8 @@ import com.webmanage.entity.UserPoints; import com.webmanage.mapper.PointsFlowMapper; import com.webmanage.mapper.UserPointsMapper; import com.webmanage.mapper.PointsTransactionMapper; import com.webmanage.entity.PointsTransaction; import com.webmanage.service.PointsFlowService; import com.webmanage.service.PointsRuleService; import lombok.extern.slf4j.Slf4j; @@ -39,6 +42,9 @@ @Resource private PointsRuleService pointsRuleService; @Resource private PointsTransactionMapper pointsTransactionMapper; @Override public PageResult<PointsFlow> getPersonalPointsFlowPage(PointsFlowQueryDTO queryDTO) { @@ -381,7 +387,7 @@ * 检查积分余额是否足够 */ private void checkBalanceSufficient(String userId, String unitId, Integer requiredPoints) { // 检查个人积分余额 // 仅检查个人积分余额(移除单位余额判定) QueryWrapper<UserPoints> userWrapper = new QueryWrapper<>(); userWrapper.eq("deleted", 0) .eq("user_id", userId); @@ -391,15 +397,7 @@ throw new BusinessException("个人积分余额不足,当前余额: " + (userPoints != null ? userPoints.getBalance() : 0) + ",需要扣除: " + requiredPoints); } // 检查单位积分余额 QueryWrapper<UserPoints> unitWrapper = new QueryWrapper<>(); unitWrapper.eq("deleted", 0) .eq("unit_id", unitId); UserPoints unitPoints = userPointsMapper.selectOne(unitWrapper); if (unitPoints == null || unitPoints.getBalance() < requiredPoints) { throw new BusinessException("单位积分余额不足,当前余额: " + (unitPoints != null ? unitPoints.getBalance() : 0) + ",需要扣除: " + requiredPoints); } // 原单位余额校验已移除 } /** @@ -566,6 +564,83 @@ } /** * 扣减用户积分 */ @Override @Transactional(rollbackFor = Exception.class) public boolean deductUserPoints(DeductUserPointsDTO deductDTO) { try { String userId = deductDTO.getUserId(); String unitId = deductDTO.getUnitId(); Integer points = deductDTO.getPoints(); String orderId = deductDTO.getOrderId(); String remark = deductDTO.getRemark(); String dataCategory = deductDTO.getDataCategory(); Integer dataType = deductDTO.getDataType(); if (!StringUtils.hasText(userId)) { throw new BusinessException("用户ID不能为空"); } if (points == null || points <= 0) { throw new BusinessException("扣减积分数量必须大于0"); } // 检查用户积分余额是否充足 checkBalanceSufficient(userId, unitId, points); // 创建积分流水记录 PointsFlow pointsFlow = new PointsFlow(); pointsFlow.setUserId(userId); pointsFlow.setUnitId(unitId); pointsFlow.setDataCategory(dataCategory != null ? dataCategory : "积分交易"); pointsFlow.setDataType(dataType != null ? dataType : 1); pointsFlow.setPoints(-points); // 负数表示扣减 pointsFlow.setName(remark != null ? remark : "积分扣减"); // name字段存储remark内容 pointsFlow.setFlowTime(LocalDateTime.now()); pointsFlow.setRlueId(null); // 直接扣减,不关联规则 boolean saved = save(pointsFlow); if (!saved) { throw new BusinessException("保存积分流水失败"); } // 更新用户积分账户 updateUserPointsOnly(userId, -points); // 新增积分交易记录(与流水同事务) PointsTransaction trans = new PointsTransaction(); // tb_points_transaction 的 chk_data_category 仅允许 '用户参与'、'其他' // 这里将业务类目映射为数据库允许的值 trans.setDataCategory("用户参与"); trans.setTransactionName(pointsFlow.getName()); trans.setTransactionTime(LocalDateTime.now()); trans.setPointsChange(-points); trans.setRuleType("消耗"); try { trans.setUserId(Long.valueOf(userId)); } catch (Exception ignore) {} try { if (StringUtils.hasText(unitId)) trans.setUnitId(Long.valueOf(unitId)); } catch (Exception ignore) {} trans.setUserType("个人用户"); trans.setRuleId(null); trans.setDetailId(null); trans.setCreatedAt(LocalDateTime.now()); trans.setDeleted(0); int inserted = pointsTransactionMapper.insert(trans); log.info("Points transaction inserted rows={}, id={}", inserted, trans.getId()); if (inserted <= 0 || trans.getId() == null) { throw new BusinessException("保存积分交易记录失败"); } return true; } catch (Exception e) { log.error("扣减用户积分失败", e); throw new BusinessException("扣减用户积分失败:" + e.getMessage()); } } /** * 仅更新提供者(单位)积分账户 */ private void updateProviderUnitPoints(String providerUnitId, Integer pointsValue) { @@ -606,4 +681,40 @@ userPointsMapper.updateById(unitPoints); } } /** * 仅更新个人积分账户(不操作单位账户) */ private void updateUserPointsOnly(String userId, Integer pointsValue) { // 更新个人积分账户 QueryWrapper<UserPoints> userWrapper = new QueryWrapper<>(); userWrapper.eq("deleted", 0) .eq("user_id", userId); UserPoints userPoints = userPointsMapper.selectOne(userWrapper); if (userPoints == null) { if (pointsValue < 0) { throw new BusinessException("个人积分余额不足,无法扣除积分"); } userPoints = new UserPoints(); userPoints.setUserId(userId); userPoints.setUnitId(null); userPoints.setBalance(pointsValue); userPoints.setTotalEarned(pointsValue > 0 ? pointsValue : 0); userPoints.setTotalConsumed(pointsValue < 0 ? Math.abs(pointsValue) : 0); userPointsMapper.insert(userPoints); } else { if (pointsValue < 0 && userPoints.getBalance() + pointsValue < 0) { throw new BusinessException("个人积分余额不足,当前余额: " + userPoints.getBalance() + ",需要扣除: " + Math.abs(pointsValue)); } userPoints.setBalance(userPoints.getBalance() + pointsValue); if (pointsValue > 0) { userPoints.setTotalEarned(userPoints.getTotalEarned() != null ? userPoints.getTotalEarned() + pointsValue : pointsValue); } if (pointsValue < 0) { userPoints.setTotalConsumed(userPoints.getTotalConsumed() != null ? userPoints.getTotalConsumed() + Math.abs(pointsValue) : Math.abs(pointsValue)); } userPoints.setUpdateTime(LocalDateTime.now()); userPointsMapper.updateById(userPoints); } } } target/classes/application-dev.yml
New file @@ -0,0 +1,69 @@ spring: # 数据源配置 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/web_manage?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: postgres password: AES:d9d2d3e0d586e76d02a97c451f3256bffdc806b4c7626904 druid: # 初始连接数 initial-size: 5 # 最小连接池数量 min-idle: 10 # 最大连接池数量 max-active: 20 # 配置获取连接等待超时的时间 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 # 配置检测连接是否有效 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 配置DruidStatFilter web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 配置DruidStatViewServlet stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: 123456 # Redis配置 redis: host: localhost port: 6379 password: database: 0 timeout: 10000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 # MinIO配置 minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: web-manage target/classes/application-prod.yml
New file @@ -0,0 +1,70 @@ # 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://192.168.20.52:5432/zypt-v2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: postgres password: AES:f9d2d3e0d586e76d14aba9d24666acef98c4605be9ec680ac4cbeede7ad2 druid: # 初始连接数 initial-size: 5 # 最小连接池数量 min-idle: 10 # 最大连接池数量 max-active: 20 # 配置获取连接等待超时的时间 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 # 配置检测连接是否有效 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 配置DruidStatFilter web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 配置DruidStatViewServlet stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: 123456 # Redis配置 redis: host: 192.168.20.51 port: 6379 password: AES:c8d9cdfddcb4b32c677d657d2c8d56f9e7e4720832656637f5 database: 4 timeout: 10000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 # MinIO配置 minio: endpoint: http://192.168.20.52:9000 access-key: minioadmin secret-key: AES:c4d4cefddd95e6733df755af0e29839c104ee8baa55eb53cdc24 part-size: 104857600 bucket-name: dev target/classes/application-test.yml
New file @@ -0,0 +1,69 @@ # 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/web_manage?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: postgres password: zkyxpostgres druid: # 初始连接数 initial-size: 5 # 最小连接池数量 min-idle: 10 # 最大连接池数量 max-active: 20 # 配置获取连接等待超时的时间 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 # 配置检测连接是否有效 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 配置DruidStatFilter web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 配置DruidStatViewServlet stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: 123456 # Redis配置 redis: host: 192.168.110.129 port: 6379 password: database: 0 timeout: 10000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 # MinIO配置 minio: endpoint: http://192.168.110.129:9000 access-key: minioadmin secret-key: minioadmin bucket-name: web-manage target/classes/application.yml
New file @@ -0,0 +1,72 @@ server: port: 8080 servlet: context-path: /admin spring: application: name: web-manage-back profiles: active: dev jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false #mcv配置 mvc: pathmatch: matching-strategy: ant_path_matcher # MyBatis Plus配置 mybatis-plus: configuration: # 开启驼峰命名 map-underscore-to-camel-case: true # 开启sql日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 主键类型 id-type: auto # 逻辑删除配置 mapper-locations: classpath*:/mapper/**/*.xml # 日志配置 logging: level: com.webmanage: debug org.springframework.web: debug pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n" # Knife4j配置 knife4j: enable: true setting: language: zh-CN enable-swagger-models: true enable-document-manage: true swagger-model-name: 实体类列表 basic: enable: false # 购物车配置 cart: # Redis缓存过期时间(天) expire-days: 30 # 是否启用数据库持久化 enable-persistence: true # 是否启用数据一致性检查 enable-consistency-check: true # 同步策略:realtime(实时同步)、batch(批量同步)、manual(手动同步) sync-strategy: realtime async: executor: core-pool-size: 4 max-pool-size: 16 queue-capacity: 500 keep-alive-seconds: 60 thread-name-prefix: async-cart- wait-for-tasks-to-complete-on-shutdown: true await-termination-seconds: 30