Browse Source

Update

stable1
B.E.N.S.O.N 2 weeks ago
parent
commit
72fe3a3ab9
3 changed files with 177 additions and 0 deletions
  1. +58
    -0
      src/main/java/com/ffii/fpsms/modules/usage/FeatureUsageLogService.java
  2. +103
    -0
      src/main/java/com/ffii/fpsms/modules/usage/web/FeatureUsageLogController.java
  3. +16
    -0
      src/main/resources/db/changelog/changes/20260420_01_Benson/01_feature_usage_log.sql

+ 58
- 0
src/main/java/com/ffii/fpsms/modules/usage/FeatureUsageLogService.java View File

@@ -0,0 +1,58 @@
package com.ffii.fpsms.modules.usage;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.support.AbstractService;
import com.ffii.core.support.JdbcDao;
import com.ffii.fpsms.modules.user.entity.User;

@Service
public class FeatureUsageLogService extends AbstractService {

public static final String FEATURE_REPORT_MANAGEMENT = "REPORT_MANAGEMENT";
public static final String FEATURE_TRUCK_ROUTING_SUMMARY = "TRUCK_ROUTING_SUMMARY";

public static final String ACTION_PAGE_VIEW = "PAGE_VIEW";
public static final String ACTION_DOWNLOAD = "DOWNLOAD";
public static final String ACTION_PRINT = "PRINT";

public FeatureUsageLogService(JdbcDao jdbcDao) {
super(jdbcDao);
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false)
public void insert(User user, String featureCode, String actionType, String detail) {
String sql = "INSERT INTO feature_usage_log (`user_id`, `username`, `feature_code`, `action_type`, `detail`) "
+ "VALUES (:userId, :username, :featureCode, :actionType, :detail)";
Map<String, Object> args = new HashMap<>(8);
args.put("userId", user.getId());
args.put("username", user.getUsername());
args.put("featureCode", featureCode);
args.put("actionType", actionType);
args.put("detail", StringUtils.isBlank(detail) ? null : StringUtils.left(detail, 512));
jdbcDao.executeUpdate(sql, args);
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
public List<Map<String, Object>> summarizeByFeature(String featureCode) {
String sql = """
SELECT user_id AS userId, username,
SUM(CASE WHEN action_type = 'PAGE_VIEW' THEN 1 ELSE 0 END) AS pageViews,
SUM(CASE WHEN action_type = 'DOWNLOAD' THEN 1 ELSE 0 END) AS downloads,
SUM(CASE WHEN action_type = 'PRINT' THEN 1 ELSE 0 END) AS prints
FROM feature_usage_log
WHERE feature_code = :featureCode
GROUP BY user_id, username
ORDER BY username
""";
return jdbcDao.queryForList(sql, Map.of("featureCode", featureCode));
}

}

+ 103
- 0
src/main/java/com/ffii/fpsms/modules/usage/web/FeatureUsageLogController.java View File

@@ -0,0 +1,103 @@
package com.ffii.fpsms.modules.usage.web;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
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.RestController;

import com.ffii.core.exception.BadRequestException;
import com.ffii.core.response.DataRes;
import com.ffii.fpsms.modules.common.SecurityUtils;
import com.ffii.fpsms.modules.usage.FeatureUsageLogService;
import com.ffii.fpsms.modules.user.entity.User;

@RestController
@RequestMapping("/feature-usage")
public class FeatureUsageLogController {

private final FeatureUsageLogService featureUsageLogService;

public FeatureUsageLogController(FeatureUsageLogService featureUsageLogService) {
this.featureUsageLogService = featureUsageLogService;
}

public static class LogRequest {
private String featureCode;
private String actionType;
private String detail;

public String getFeatureCode() {
return featureCode;
}

public void setFeatureCode(String featureCode) {
this.featureCode = featureCode;
}

public String getActionType() {
return actionType;
}

public void setActionType(String actionType) {
this.actionType = actionType;
}

public String getDetail() {
return detail;
}

public void setDetail(String detail) {
this.detail = detail;
}
}

@PostMapping("/log")
public ResponseEntity<Void> log(@RequestBody LogRequest body) {
User user = SecurityUtils.getUser().orElseThrow(() -> new BadRequestException("Not authenticated"));
validateLogRequest(body);
featureUsageLogService.insert(user, body.getFeatureCode(), body.getActionType(), body.getDetail());
return ResponseEntity.status(HttpStatus.CREATED).build();
}

private void validateLogRequest(LogRequest body) {
if (body == null || StringUtils.isBlank(body.getFeatureCode()) || StringUtils.isBlank(body.getActionType())) {
throw new BadRequestException("featureCode and actionType are required");
}
String fc = body.getFeatureCode();
String at = body.getActionType();
if (!FeatureUsageLogService.FEATURE_REPORT_MANAGEMENT.equals(fc)
&& !FeatureUsageLogService.FEATURE_TRUCK_ROUTING_SUMMARY.equals(fc)) {
throw new BadRequestException("Invalid featureCode");
}
if (!FeatureUsageLogService.ACTION_PAGE_VIEW.equals(at)
&& !FeatureUsageLogService.ACTION_DOWNLOAD.equals(at)
&& !FeatureUsageLogService.ACTION_PRINT.equals(at)) {
throw new BadRequestException("Invalid actionType");
}
if (FeatureUsageLogService.ACTION_PRINT.equals(at)
&& !FeatureUsageLogService.FEATURE_TRUCK_ROUTING_SUMMARY.equals(fc)) {
throw new BadRequestException("PRINT is only valid for TRUCK_ROUTING_SUMMARY");
}
}

@GetMapping("/summary")
@PreAuthorize("hasAnyAuthority('TESTING','ADMIN')")
public DataRes<Map<String, List<Map<String, Object>>>> summary() {
Map<String, List<Map<String, Object>>> data = new HashMap<>(2);
data.put("reportManagement",
featureUsageLogService.summarizeByFeature(FeatureUsageLogService.FEATURE_REPORT_MANAGEMENT));
data.put("truckRoutingSummary",
featureUsageLogService.summarizeByFeature(FeatureUsageLogService.FEATURE_TRUCK_ROUTING_SUMMARY));
return new DataRes<>(data);
}

}

+ 16
- 0
src/main/resources/db/changelog/changes/20260420_01_Benson/01_feature_usage_log.sql View File

@@ -0,0 +1,16 @@
--liquibase formatted sql

--changeset codex:feature_usage_log
--comment: Track report management and truck routing summary page views, downloads, and prints
CREATE TABLE `feature_usage_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`username` varchar(64) NOT NULL,
`feature_code` varchar(64) NOT NULL,
`action_type` varchar(32) NOT NULL,
`detail` varchar(512) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_feature_usage_feature_created` (`feature_code`, `created_at`),
KEY `idx_feature_usage_user_feature` (`user_id`, `feature_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Loading…
Cancel
Save