Commit 4bf8eaf1 by jianan

操作日志1

parent dd86ab31
package com.yd.csf.api.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yd.common.result.Result;
import com.yd.csf.api.dto.AuditLogQueryRequest;
import com.yd.csf.service.model.SysOperDataChange;
import com.yd.csf.service.model.SysOperDataSnapshot;
import com.yd.csf.service.model.SysOperLog;
import com.yd.csf.service.service.AuditLogDataChangeService;
import com.yd.csf.service.service.AuditLogDataSnapshotService;
import com.yd.csf.service.service.AuditLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/auditlog")
@Tag(name = "审计日志")
@RequiredArgsConstructor
public class AuditLogController {
@Resource
private AuditLogService operLogService;
@Resource
private AuditLogDataSnapshotService snapshotService;
@Resource
private AuditLogDataChangeService changeService;
@PostMapping("/log/page")
@Operation(summary = "操作日志分页查询")
public Result<Page<SysOperLog>> page(@RequestBody AuditLogQueryRequest request) {
LambdaQueryWrapper<SysOperLog> wrapper = new LambdaQueryWrapper<SysOperLog>()
.eq(StringUtils.isNotBlank(request.getModule()), SysOperLog::getModule, request.getModule())
.eq(StringUtils.isNotBlank(request.getOperName()), SysOperLog::getOperName, request.getOperName())
.eq(request.getStatus() != null, SysOperLog::getStatus, request.getStatus())
.ge(StringUtils.isNotBlank(request.getStartTime()), SysOperLog::getOperTime, request.getStartTime())
.le(StringUtils.isNotBlank(request.getEndTime()), SysOperLog::getOperTime, request.getEndTime())
.orderByDesc(SysOperLog::getOperTime);
Page<SysOperLog> page = operLogService.page(
new Page<>(request.getPageNo(), request.getPageSize()), wrapper);
return Result.success(page);
}
@GetMapping("/log/detail")
@Operation(summary = "审计详情查询")
public Result<Map<String, Object>> detail(@RequestParam String requestId) {
if (StringUtils.isBlank(requestId)) {
return Result.success(null);
}
SysOperLog operLog = operLogService.getOne(
new LambdaQueryWrapper<SysOperLog>().eq(SysOperLog::getRequestId, requestId));
List<SysOperDataSnapshot> snapshots = snapshotService.list(
new LambdaQueryWrapper<SysOperDataSnapshot>().eq(SysOperDataSnapshot::getRequestId, requestId));
List<SysOperDataChange> changes = changeService.list(
new LambdaQueryWrapper<SysOperDataChange>().eq(SysOperDataChange::getRequestId, requestId));
Map<String, Object> result = new HashMap<>();
result.put("operLog", operLog);
result.put("snapshots", snapshots);
result.put("changes", changes);
return Result.success(result);
}
}
package com.yd.csf.service.audit;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@Accessors(chain = true)
public class AuditContext {
private String requestId;
private String module;
private Integer businessType;
private String method;
private String content;
private Long businessId;
private Integer operationType;
private Long userId;
private String operName;
private String deptName;
private String operUrl;
private String operIp;
private String operLocation;
private String operParam;
private String jsonResult;
private Integer status;
private String errorMsg;
private Date operTime;
}
package com.yd.csf.service.audit;
public class AuditContextHolder {
private static final ThreadLocal<AuditContext> CONTEXT = new ThreadLocal<>();
public static void set(AuditContext context) {
CONTEXT.set(context);
}
public static AuditContext get() {
return CONTEXT.get();
}
public static String getRequestId() {
AuditContext ctx = CONTEXT.get();
return ctx != null ? ctx.getRequestId() : null;
}
public static void clear() {
CONTEXT.remove();
}
}
package com.yd.csf.service.audit;
import cn.hutool.core.collection.CollUtil;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.yd.csf.service.annotation.Auditable;
import com.yd.csf.service.model.SysOperDataChange;
import com.yd.csf.service.model.SysOperDataSnapshot;
import com.yd.csf.service.service.AuditLogDataChangeService;
import com.yd.csf.service.service.AuditLogDataSnapshotService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.*;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuditHelper {
private final AuditLogDataSnapshotService snapshotService;
private final AuditLogDataChangeService changeService;
private final FieldLabelResolver fieldLabelResolver;
private static final Set<String> EXCLUDED_FIELDS = new HashSet<>(Arrays.asList(
"id", "createTime", "updateTime", "creatorId", "creatorName",
"updaterId", "updaterName", "isDeleted", "serialVersionUID"
));
private static final Gson GSON = new GsonBuilder()
.addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return EXCLUDED_FIELDS.contains(f.getName());
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
})
.create();
/**
* 记录数据变更。
*
* @param tableName 表名
* @param beforeMap 变更前的数据,key 为主键值,value 为实体对象(INSERT 时为 null)
* @param afterMap 变更后的数据,key 为主键值,value 为实体对象(DELETE 时为 null)
* @param ctx 审计上下文
*/
public void record(String tableName, Map<String, ?> beforeMap, Map<String, ?> afterMap, AuditContext ctx) {
if (beforeMap == null && afterMap == null) {
return;
}
Class<?> entityClass = getEntityClass(beforeMap, afterMap);
if (entityClass == null) {
log.warn("无法确定实体类型,跳过审计记录: table={}", tableName);
return;
}
Auditable auditable = entityClass.getAnnotation(Auditable.class);
if (auditable == null) {
throw new IllegalArgumentException("实体类 " + entityClass.getName() + " 缺少 @Auditable 注解");
}
List<Field> diffFields = getDiffFields(entityClass);
Set<String> allIds = new LinkedHashSet<>();
if (beforeMap != null) allIds.addAll(beforeMap.keySet());
if (afterMap != null) allIds.addAll(afterMap.keySet());
List<SysOperDataSnapshot> snapshots = new ArrayList<>();
List<SysOperDataChange> changes = new ArrayList<>();
Date now = new Date();
for (String recordId : allIds) {
Object before = beforeMap != null ? beforeMap.get(recordId) : null;
Object after = afterMap != null ? afterMap.get(recordId) : null;
String beforeJson = before != null ? GSON.toJson(before) : null;
String afterJson = after != null ? GSON.toJson(after) : null;
boolean isInsert = before == null && after != null;
boolean isDelete = before != null && after == null;
String changeType = isInsert ? "ADD" : (isDelete ? "DELETE" : "MODIFY");
SysOperDataSnapshot snapshot = new SysOperDataSnapshot();
snapshot.setRequestId(ctx.getRequestId());
snapshot.setTableName(tableName);
snapshot.setRecordId(recordId);
snapshot.setBeforeJson(beforeJson);
snapshot.setAfterJson(afterJson);
snapshot.setCreateTime(now);
snapshots.add(snapshot);
if (!isInsert && !isDelete && before != null && after != null) {
for (Field field : diffFields) {
try {
field.setAccessible(true);
Object oldVal = field.get(before);
Object newVal = field.get(after);
if (!Objects.equals(oldVal, newVal)) {
SysOperDataChange change = new SysOperDataChange();
change.setRequestId(ctx.getRequestId());
change.setTableName(tableName);
change.setRecordId(recordId);
change.setFieldName(field.getName());
change.setFieldComment(fieldLabelResolver.resolve(field, tableName));
change.setOldValue(oldVal != null ? oldVal.toString() : null);
change.setNewValue(newVal != null ? newVal.toString() : null);
change.setChangeType("MODIFY");
change.setCreateTime(now);
changes.add(change);
}
} catch (IllegalAccessException e) {
log.debug("跳过无法访问的字段: {}", field.getName());
}
}
}
}
if (CollUtil.isNotEmpty(snapshots)) {
snapshotService.saveBatch(snapshots);
}
if (CollUtil.isNotEmpty(changes)) {
changeService.saveBatch(changes);
}
}
private Class<?> getEntityClass(Map<String, ?> beforeMap, Map<String, ?> afterMap) {
if (beforeMap != null) {
for (Object v : beforeMap.values()) {
if (v != null) return v.getClass();
}
}
if (afterMap != null) {
for (Object v : afterMap.values()) {
if (v != null) return v.getClass();
}
}
return null;
}
private List<Field> getDiffFields(Class<?> entityClass) {
List<Field> fields = new ArrayList<>();
Class<?> current = entityClass;
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (!EXCLUDED_FIELDS.contains(field.getName())
&& !java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
fields.add(field);
}
}
current = current.getSuperclass();
}
return fields;
}
/**
* 对实体列表做深拷贝快照。
* 使用 Gson 序列化→反序列化做深拷贝,自动从 @Auditable.idField 读取 Map key。
*
* @param entities 实体列表,null 或空列表返回空 Map
* @param <T> 实体类型
* @return Map&lt;业务ID, 深拷贝后的实体&gt;
* @throws IllegalArgumentException 如果实体类缺少 @Auditable 注解
*/
@SuppressWarnings("unchecked")
public <T> Map<String, T> snapshot(List<T> entities) {
if (CollUtil.isEmpty(entities)) {
return Collections.emptyMap();
}
T first = entities.get(0);
if (first == null) {
return Collections.emptyMap();
}
Class<?> entityClass = first.getClass();
Auditable auditable = entityClass.getAnnotation(Auditable.class);
if (auditable == null) {
throw new IllegalArgumentException("实体类 " + entityClass.getName() + " 缺少 @Auditable 注解");
}
String idFieldName = auditable.idField();
Map<String, T> result = new LinkedHashMap<>();
for (T entity : entities) {
try {
Field idField = findField(entityClass, idFieldName);
if (idField == null) {
throw new RuntimeException("实体类 " + entityClass.getName() + " 中没有字段: " + idFieldName);
}
idField.setAccessible(true);
Object idValue = idField.get(entity);
if (idValue == null) {
log.warn("实体 idField={} 值为 null,跳过: {}", idFieldName, entity);
continue;
}
T copy = (T) GSON.fromJson(GSON.toJson(entity), entityClass);
result.put(idValue.toString(), copy);
} catch (IllegalAccessException e) {
throw new RuntimeException("无法读取字段 " + idFieldName, e);
}
}
return result;
}
private Field findField(Class<?> clazz, String fieldName) {
Class<?> current = clazz;
while (current != null && current != Object.class) {
try {
return current.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
current = current.getSuperclass();
}
}
return null;
}
}
package com.yd.csf.service.audit;
import com.yd.auth.core.dto.AuthUserDto;
import com.yd.auth.core.utils.SecurityUtil;
import com.yd.csf.service.model.SysOperLog;
import com.yd.csf.service.service.AuditLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.UUID;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuditService {
private final AuditLogService operLogService;
/**
* 开始审计,创建新的审计上下文。
*
* @param module 业务模块
* @param businessType 业务类型
* @param content 操作描述
* @return requestId
*/
public String begin(String module, Integer businessType, String content) {
String requestId = UUID.randomUUID().toString().replace("-", "");
AuditContext ctx = new AuditContext();
ctx.setRequestId(requestId);
ctx.setModule(module);
ctx.setBusinessType(businessType);
ctx.setContent(content);
ctx.setOperTime(new Date());
fillRequestInfo(ctx);
fillOperatorInfo(ctx);
AuditContextHolder.set(ctx);
return requestId;
}
/**
* 使用已有 requestId 恢复审计上下文(用于异步回调)。
*/
public void begin(String requestId) {
AuditContext ctx = new AuditContext();
ctx.setRequestId(requestId);
ctx.setOperTime(new Date());
AuditContextHolder.set(ctx);
}
/**
* 标记操作成功,写入 sys_oper_log。
*/
public void success(String requestId) {
AuditContext ctx = AuditContextHolder.get();
if (ctx == null) {
return;
}
SysOperLog log = buildOperLog(ctx);
log.setStatus(0);
operLogService.save(log);
}
/**
* 标记操作失败,写入 sys_oper_log。
*/
public void fail(String requestId, String errorMsg) {
AuditContext ctx = AuditContextHolder.get();
if (ctx == null) {
return;
}
SysOperLog log = buildOperLog(ctx);
log.setStatus(1);
log.setErrorMsg(errorMsg);
operLogService.save(log);
}
/**
* 关闭审计,清理 ThreadLocal。
*/
public void close(String requestId) {
AuditContextHolder.clear();
}
private void fillRequestInfo(AuditContext ctx) {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
ctx.setOperUrl(request.getRequestURI());
ctx.setOperIp(request.getRemoteAddr());
}
} catch (Exception e) {
log.debug("获取请求信息失败: {}", e.getMessage());
}
}
private void fillOperatorInfo(AuditContext ctx) {
try {
AuthUserDto user = SecurityUtil.getCurrentLoginUser();
ctx.setUserId(user.getId());
ctx.setOperName(user.getUsername());
} catch (Exception e) {
log.debug("获取操作人信息失败: {}", e.getMessage());
}
}
private SysOperLog buildOperLog(AuditContext ctx) {
SysOperLog log = new SysOperLog();
log.setRequestId(ctx.getRequestId());
log.setModule(ctx.getModule());
log.setBusinessType(ctx.getBusinessType());
log.setMethod(ctx.getMethod());
log.setContent(ctx.getContent());
log.setBusinessId(ctx.getBusinessId());
log.setOperationType(ctx.getOperationType());
log.setUserId(ctx.getUserId());
log.setOperName(ctx.getOperName());
log.setDeptName(ctx.getDeptName());
log.setOperUrl(ctx.getOperUrl());
log.setOperIp(ctx.getOperIp());
log.setOperLocation(ctx.getOperLocation());
log.setOperParam(ctx.getOperParam());
log.setJsonResult(ctx.getJsonResult());
log.setOperTime(ctx.getOperTime());
return log;
}
}
package com.yd.csf.service.audit;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 审计生命周期编排器。
* 封装 begin → action → success/fail → close 的固定模板,调用方只需编写业务逻辑。
*
* <pre>{@code
* // INSERT 场景
* return auditTemplate.execute("来佣管理", 1, "导入入账记录", ctx -> {
* saveBatch(list);
* auditHelper.record("commission", null, toMap(list), ctx);
* return true;
* });
*
* // UPDATE 场景
* return auditTemplate.execute("应付款管理", 2, "生成可出账", ctx -> {
* Map<String, ExpectedFortune> before = auditHelper.snapshot(list);
* // ... 业务变更 ...
* Map<String, ExpectedFortune> after = auditHelper.snapshot(list);
* auditHelper.record("expected_fortune", before, after, ctx);
* return true;
* });
* }</pre>
*
* @author Zhang Jianan
*/
@Component
@RequiredArgsConstructor
public class AuditTemplate {
private final AuditService auditService;
/**
* 执行带审计的操作。
* 自动处理 begin → success → close(正常路径)或 begin → fail → close(异常路径)。
* 异常会原样重新抛出,不改变异常传播链。
*
* @param module 业务模块名称
* @param businessType 业务类型
* @param content 操作描述
* @param action 业务逻辑,接收 AuditContext,返回结果
* @param <T> 返回值类型
* @return action 的返回值
*/
public <T> T execute(String module, Integer businessType,
String content, AuditAction<T> action) {
String requestId = auditService.begin(module, businessType, content);
try {
T result = action.apply(AuditContextHolder.get());
auditService.success(requestId);
return result;
} catch (Exception e) {
auditService.fail(requestId, e.getMessage());
throw (RuntimeException) e;
} finally {
auditService.close(requestId);
}
}
/**
* 审计操作函数式接口。
*
* @param <T> 返回值类型
*/
@FunctionalInterface
public interface AuditAction<T> {
T apply(AuditContext ctx);
}
}
package com.yd.csf.service.audit;
import com.yd.csf.service.annotation.AuditField;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@RequiredArgsConstructor
public class FieldLabelResolver {
private final JdbcTemplate jdbcTemplate;
private final Map<String, String> cache = new ConcurrentHashMap<>();
private static final String SQL = "SELECT COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?";
/**
* 获取字段中文名,优先级:
* 1. @AuditField.label
* 2. information_schema.COLUMNS.COLUMN_COMMENT
* 3. field 名本身
*/
public String resolve(Field field, String tableName) {
AuditField auditField = field.getAnnotation(AuditField.class);
if (auditField != null && !auditField.label().isEmpty()) {
return auditField.label();
}
String columnName = camelToUnderscore(field.getName());
String cacheKey = tableName + "." + columnName;
String comment = cache.computeIfAbsent(cacheKey, k -> {
try {
return jdbcTemplate.queryForObject(SQL, String.class, tableName, columnName);
} catch (Exception e) {
return null;
}
});
if (comment != null && !comment.isEmpty()) {
return comment;
}
return field.getName();
}
private String camelToUnderscore(String camel) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < camel.length(); i++) {
char c = camel.charAt(i);
if (Character.isUpperCase(c)) {
sb.append('_').append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
}
package com.yd.csf.service.component;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yd.auth.core.dto.AuthUserDto;
import com.yd.auth.core.utils.SecurityUtil;
import com.yd.common.enums.ResultCode;
import com.yd.common.exception.BusinessException;
import com.yd.csf.service.audit.AuditContextHolder;
import com.yd.csf.service.audit.AuditHelper;
import com.yd.csf.service.audit.AuditService;
import com.yd.csf.service.enums.CommissionExpectedStatusEnum;
import com.yd.csf.service.enums.CommissionStatusEnum;
import com.yd.csf.service.model.Commission;
......@@ -24,9 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
......@@ -39,10 +41,24 @@ public class CommissionAsyncService {
private CommissionService commissionService;
@Resource
private CommissionCompareRecordService commissionCompareRecordService;
@Resource
private AuditService auditService;
@Resource
private AuditHelper auditHelper;
// @Async("commonAsyncExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void commissionCompareBatch(List<Commission> entities) {
// 保留旧签名兼容(无审计)
commissionCompareBatch(entities, null);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void commissionCompareBatch(List<Commission> entities, String requestId) {
if (requestId != null) {
auditService.begin(requestId);
}
try {
// 根据是否关联保单号,来区分比对方式
List<Commission> hasPolicyNoList = new ArrayList<>();
List<Commission> noPolicyNoList = new ArrayList<>();
......@@ -53,12 +69,118 @@ public class CommissionAsyncService {
hasPolicyNoList.add(entity);
}
}
// 审计:收集将被修改的记录ID,拍快照
Set<String> commissionBizIds = new HashSet<>();
Set<String> expectedBizIds = new HashSet<>();
collectAffectedIds(hasPolicyNoList, noPolicyNoList, commissionBizIds, expectedBizIds);
Map<String, Commission> commissionBefore = queryCommissionsByBizIds(commissionBizIds);
Map<String, CommissionExpected> expectedBefore = queryExpectedByBizIds(expectedBizIds);
// 执行比对
if (CollectionUtils.isNotEmpty(hasPolicyNoList)) {
normalBatch(hasPolicyNoList);
}
if (CollectionUtils.isNotEmpty(noPolicyNoList)) {
noPolicyNoBatch(noPolicyNoList);
}
// 审计:记录变更(不写 oper_log,主事务已写)
if (requestId != null) {
Map<String, Commission> commissionAfter = queryCommissionsByBizIds(commissionBizIds);
Map<String, CommissionExpected> expectedAfter = queryExpectedByBizIds(expectedBizIds);
if (CollUtil.isNotEmpty(commissionBefore) && CollUtil.isNotEmpty(commissionAfter)) {
auditHelper.record("commission", commissionBefore, commissionAfter, AuditContextHolder.get());
}
if (CollUtil.isNotEmpty(expectedBefore) && CollUtil.isNotEmpty(expectedAfter)) {
auditHelper.record("commission_expected", expectedBefore, expectedAfter, AuditContextHolder.get());
}
}
} catch (Exception e) {
throw e;
} finally {
if (requestId != null) {
auditService.close(requestId);
}
}
}
/**
* 收集比对过程中会被修改的 commission 和 commission_expected 记录ID。
*/
private void collectAffectedIds(List<Commission> hasPolicyNoList, List<Commission> noPolicyNoList,
Set<String> commissionBizIds, Set<String> expectedBizIds) {
// 新插入的 commission
for (Commission entity : hasPolicyNoList) {
if (entity.getCommissionBizId() != null) {
commissionBizIds.add(entity.getCommissionBizId());
}
}
for (Commission entity : noPolicyNoList) {
if (entity.getCommissionBizId() != null) {
commissionBizIds.add(entity.getCommissionBizId());
}
}
// normalBatch: 查询同一 expectedBizId 下的所有已存在 commission
if (CollectionUtils.isNotEmpty(hasPolicyNoList)) {
List<String> commissionExpectedBizIdList = hasPolicyNoList.stream()
.map(Commission::getCommissionExpectedBizId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(commissionExpectedBizIdList)) {
List<Commission> existing = commissionService.lambdaQuery()
.in(Commission::getCommissionExpectedBizId, commissionExpectedBizIdList)
.list();
existing.forEach(c -> {
if (c.getCommissionBizId() != null) {
commissionBizIds.add(c.getCommissionBizId());
}
});
expectedBizIds.addAll(commissionExpectedBizIdList);
}
}
// noPolicyNoBatch: 查询同一 receivableNo 的 commissionExpected
if (CollectionUtils.isNotEmpty(noPolicyNoList)) {
List<String> receivableNoList = noPolicyNoList.stream()
.map(Commission::getReceivableNo)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(receivableNoList)) {
List<CommissionExpected> expectedList = commissionExpectedService.lambdaQuery()
.in(CommissionExpected::getReceivableNo, receivableNoList)
.list();
expectedList.forEach(e -> {
if (e.getCommissionExpectedBizId() != null) {
expectedBizIds.add(e.getCommissionExpectedBizId());
}
});
}
}
}
private Map<String, Commission> queryCommissionsByBizIds(Set<String> bizIds) {
if (CollectionUtils.isEmpty(bizIds)) {
return Collections.emptyMap();
}
List<Commission> list = commissionService.lambdaQuery()
.in(Commission::getCommissionBizId, new ArrayList<>(bizIds))
.list();
return list.stream().collect(Collectors.toMap(Commission::getCommissionBizId, Function.identity()));
}
private Map<String, CommissionExpected> queryExpectedByBizIds(Set<String> bizIds) {
if (CollectionUtils.isEmpty(bizIds)) {
return Collections.emptyMap();
}
List<CommissionExpected> list = commissionExpectedService.lambdaQuery()
.in(CommissionExpected::getCommissionExpectedBizId, new ArrayList<>(bizIds))
.list();
return list.stream().collect(Collectors.toMap(CommissionExpected::getCommissionExpectedBizId, Function.identity()));
}
/**
......
package com.yd.csf.service.dao;
import com.yd.csf.service.model.SysOperDataChange;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_change】的数据库操作Mapper
* @createDate 2026-07-30 14:46:28
* @Entity generator.domain.SysOperDataChange
*/
public interface AuditLogDataChangeMapper extends BaseMapper<SysOperDataChange> {
}
\ No newline at end of file
package com.yd.csf.service.dao;
import com.yd.csf.service.model.SysOperDataSnapshot;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_snapshot】的数据库操作Mapper
* @createDate 2026-07-30 14:46:24
* @Entity generator.domain.SysOperDataSnapshot
*/
public interface AuditLogDataSnapshotMapper extends BaseMapper<SysOperDataSnapshot> {
}
\ No newline at end of file
package com.yd.csf.service.dao;
import com.yd.csf.service.model.SysOperLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_log】的数据库操作Mapper
* @createDate 2026-07-30 14:45:38
* @Entity generator.domain.SysOperLog
*/
public interface AuditLogMapper extends BaseMapper<SysOperLog> {
}
\ No newline at end of file
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.*;
import com.yd.csf.service.annotation.Auditable;
import lombok.Data;
import java.io.Serializable;
......@@ -13,6 +14,7 @@ import java.util.Date;
* @TableName commission
*/
@TableName(value = "commission")
@Auditable(table = "commission", idField = "commissionBizId")
@Data
public class Commission implements Serializable {
/**
......
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.*;
import com.yd.csf.service.annotation.Auditable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
......@@ -14,6 +15,7 @@ import java.util.Date;
* @TableName commission_expected
*/
@TableName(value = "commission_expected")
@Auditable(table = "commission_expected", idField = "commissionExpectedBizId")
@Data
public class CommissionExpected implements Serializable {
/**
......
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.*;
import com.yd.csf.service.annotation.Auditable;
import lombok.Getter;
import lombok.Setter;
......@@ -20,6 +21,7 @@ import java.time.LocalDateTime;
@Getter
@Setter
@TableName("expected_fortune")
@Auditable(table = "expected_fortune", idField = "expectedFortuneBizId")
public class ExpectedFortune implements Serializable {
private static final long serialVersionUID = 1L;
......
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.*;
import com.yd.csf.service.annotation.Auditable;
import java.math.BigDecimal;
import java.time.LocalDate;
......@@ -14,6 +15,7 @@ import lombok.Data;
* @TableName fortune
*/
@TableName(value = "fortune")
@Auditable(table = "fortune", idField = "fortuneBizId")
@Data
public class Fortune {
/**
......
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName sys_oper_data_change
*/
@TableName(value ="sys_oper_data_change")
@Data
public class SysOperDataChange {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*/
private String requestId;
/**
* 关联 sys_oper_log.id(MP拦截器路径不设置,由request_id关联)
*/
private Long operLogId;
/**
* 表名
*/
private String tableName;
/**
* 记录主键
*/
private String recordId;
/**
* 字段名
*/
private String fieldName;
/**
* 字段中文名
*/
private String fieldComment;
/**
* 变更前值
*/
private String oldValue;
/**
* 变更后值
*/
private String newValue;
/**
* ADD/MODIFY/DELETE
*/
private String changeType;
/**
*
*/
private Date createTime;
}
\ No newline at end of file
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName sys_oper_data_snapshot
*/
@TableName(value ="sys_oper_data_snapshot")
@Data
public class SysOperDataSnapshot {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*/
private String requestId;
/**
* 表名
*/
private String tableName;
/**
* 记录主键
*/
private String recordId;
/**
* 变更前完整快照
*/
private String beforeJson;
/**
* 变更后完整快照
*/
private String afterJson;
/**
*
*/
private Date createTime;
}
\ No newline at end of file
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName sys_oper_log
*/
@TableName(value ="sys_oper_log")
@Data
public class SysOperLog {
/**
* 日志主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*/
private String requestId;
/**
* 业务模块
*/
private String module;
/**
* 业务类型 0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据
*/
private Integer businessType;
/**
* 方法名称
*/
private String method;
/**
* 日志内容
*/
private String content;
/**
* 业务数据ID
*/
private Long businessId;
/**
* 操作类型(0其它 1新增 2修改 3删除)
*/
private Integer operationType;
/**
* 操作人ID
*/
private Long userId;
/**
* 操作人员
*/
private String operName;
/**
* 部门名称
*/
private String deptName;
/**
* 请求URL
*/
private String operUrl;
/**
* 主机地址
*/
private String operIp;
/**
* 操作地点
*/
private String operLocation;
/**
* 请求参数
*/
private String operParam;
/**
* 返回参数
*/
private String jsonResult;
/**
* 0成功 1失败
*/
private Integer status;
/**
* 错误消息
*/
private String errorMsg;
/**
* 操作时间
*/
private Date operTime;
}
\ No newline at end of file
package com.yd.csf.service.service;
import com.yd.csf.service.model.SysOperDataChange;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_change】的数据库操作Service
* @createDate 2026-07-30 14:46:28
*/
public interface AuditLogDataChangeService extends IService<SysOperDataChange> {
}
package com.yd.csf.service.service;
import com.yd.csf.service.model.SysOperDataSnapshot;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_snapshot】的数据库操作Service
* @createDate 2026-07-30 14:46:24
*/
public interface AuditLogDataSnapshotService extends IService<SysOperDataSnapshot> {
}
package com.yd.csf.service.service;
import com.yd.csf.service.model.SysOperLog;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_log】的数据库操作Service
* @createDate 2026-07-30 14:45:38
*/
public interface AuditLogService extends IService<SysOperLog> {
}
package com.yd.csf.service.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.csf.service.model.SysOperDataChange;
import com.yd.csf.service.service.AuditLogDataChangeService;
import com.yd.csf.service.dao.AuditLogDataChangeMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_change】的数据库操作Service实现
* @createDate 2026-07-30 14:46:28
*/
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class AuditLogDataChangeServiceImpl extends ServiceImpl<AuditLogDataChangeMapper, SysOperDataChange>
implements AuditLogDataChangeService {
}
package com.yd.csf.service.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.csf.service.model.SysOperDataSnapshot;
import com.yd.csf.service.service.AuditLogDataSnapshotService;
import com.yd.csf.service.dao.AuditLogDataSnapshotMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_data_snapshot】的数据库操作Service实现
* @createDate 2026-07-30 14:46:24
*/
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class AuditLogDataSnapshotServiceImpl extends ServiceImpl<AuditLogDataSnapshotMapper, SysOperDataSnapshot>
implements AuditLogDataSnapshotService {
}
package com.yd.csf.service.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.csf.service.model.SysOperLog;
import com.yd.csf.service.service.AuditLogService;
import com.yd.csf.service.dao.AuditLogMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Zhang Jianan
* @description 针对表【sys_oper_log】的数据库操作Service实现
* @createDate 2026-07-30 14:45:38
*/
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class AuditLogServiceImpl extends ServiceImpl<AuditLogMapper, SysOperLog>
implements AuditLogService {
}
......@@ -24,6 +24,10 @@ import com.yd.csf.service.enums.CommissionStatusEnum;
import com.yd.csf.service.enums.FortuneStatusEnum;
import com.yd.csf.service.model.*;
import com.yd.csf.service.service.*;
import com.yd.csf.service.audit.AuditContextHolder;
import com.yd.csf.service.audit.AuditHelper;
import com.yd.csf.service.audit.AuditService;
import com.yd.csf.service.audit.AuditTemplate;
import com.yd.csf.service.dao.CommissionMapper;
import com.yd.csf.service.vo.CommissionStatisticsVO;
import com.yd.csf.service.vo.CommissionVO;
......@@ -85,6 +89,12 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
private ApiInsuranceReconciliationCompanyFeignClient apiInsuranceReconciliationCompanyFeignClient;
@Resource
private PolicyPolicyholderService policyPolicyholderService;
@Resource
private AuditService auditService;
@Resource
private AuditHelper auditHelper;
@Resource
private AuditTemplate auditTemplate;
@Override
......@@ -608,6 +618,7 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean generateFortune(GenerateFortuneRequest generateFortuneRequest) {
return auditTemplate.execute("应付款管理", 2, "生成可出账记录", ctx -> {
List<String> commissionBizIdList = generateFortuneRequest.getCommissionBizIdList();
List<Commission> commissions = this.lambdaQuery().in(Commission::getCommissionBizId, commissionBizIdList).list();
......@@ -708,13 +719,40 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
// 2.5 根据保单号、期数查询入账检核汇率
Map<String, BigDecimal> exchangeRateMap = this.queryCommissionExchangeRateMap(commissions);
// 审计:深拷贝预计发佣记录变更前快照
Map<String, ExpectedFortune> beforeExpectedFortuneMap =
auditHelper.snapshot(filteredExpectedFortuneList2);
// 3. 构建实际的初始发佣记录(使用入账检核汇率)
List<Fortune> newFortuneList = buildNewFortunes(filteredExpectedFortuneList2, commissions, exchangeRateMap, reconciliationYearMonth);
// 4. 保存发佣记录
saveNewFortunes(newFortuneList);
// 审计:fortune DELETE
if (CollUtil.isNotEmpty(deleteList)) {
Map<String, Fortune> deleteMap = deleteList.stream()
.collect(Collectors.toMap(Fortune::getFortuneBizId, Function.identity()));
auditHelper.record("fortune", deleteMap, null, ctx);
}
// 审计:fortune INSERT
Map<String, Fortune> insertMap = newFortuneList.stream()
.collect(Collectors.toMap(Fortune::getFortuneBizId, Function.identity()));
auditHelper.record("fortune", null, insertMap, ctx);
// 审计:expected_fortune UPDATE(重新查询 after 状态后 diff)
List<String> efBizIds = new ArrayList<>(beforeExpectedFortuneMap.keySet());
if (CollUtil.isNotEmpty(efBizIds)) {
Map<String, ExpectedFortune> afterExpectedFortuneMap = iExpectedFortuneService.lambdaQuery()
.in(ExpectedFortune::getExpectedFortuneBizId, efBizIds)
.list().stream()
.collect(Collectors.toMap(ExpectedFortune::getExpectedFortuneBizId, Function.identity()));
auditHelper.record("expected_fortune", beforeExpectedFortuneMap, afterExpectedFortuneMap, ctx);
}
return true;
});
}
private Map<String, BigDecimal> queryCommissionExchangeRateMap(List<Commission> commissions) {
......@@ -991,6 +1029,8 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
if (CollectionUtils.isEmpty(customerAddRequestList)) {
throw new BusinessException(ResultCode.PARAMS_ERROR.getCode(), "入账数据不能为空");
}
return auditTemplate.execute("来佣管理", 1, "导入入账记录", ctx -> {
validateAddCommissionBatch(customerAddRequestList);
Set<String> policyNoSet = customerAddRequestList.stream()
......@@ -1057,23 +1097,29 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
// 1. 先保存主数据
saveOrUpdateBatch(commissionList);
// 审计:commission INSERT
if (CollUtil.isNotEmpty(commissionList)) {
Map<String, Commission> insertMap = commissionList.stream()
.collect(Collectors.toMap(Commission::getCommissionBizId, Function.identity()));
auditHelper.record("commission", null, insertMap, ctx);
}
// 2. 事务提交后执行比对
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
try {
// 这里会开启新事务执行更新
commissionAsyncService.commissionCompareBatch(commissionList);
commissionAsyncService.commissionCompareBatch(commissionList, ctx.getRequestId());
} catch (Exception e) {
log.error("比对更新失败", e);
// 可以发送告警或记录失败任务
}
}
}
);
return commissionList;
});
}
private CommissionExpected getCommissionExpectedBizIdBatch(CommissionAddRequest request, Set<String> policyNoSet, Commission commission) {
......@@ -1311,8 +1357,8 @@ public class CommissionServiceImpl extends ServiceImpl<CommissionMapper, Commiss
@Override
public Long queryCount(String commissionExpectedBizId) {
return this.baseMapper.selectCount(new LambdaQueryWrapper<Commission>()
.eq(Commission::getCommissionExpectedBizId,commissionExpectedBizId)
.eq(Commission::getIsDeleted,0)
.eq(Commission::getCommissionExpectedBizId, commissionExpectedBizId)
.eq(Commission::getIsDeleted, 0)
);
}
......
......@@ -18,6 +18,11 @@ import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result;
import com.yd.common.utils.RandomStringGenerator;
import com.yd.common.utils.RedisUtil;
import com.yd.csf.service.audit.AuditContextHolder;
import com.yd.csf.service.audit.AuditHelper;
import com.yd.csf.service.audit.AuditService;
import com.yd.csf.service.audit.AuditTemplate;
import com.yd.csf.service.audit.AuditContext;
import com.yd.csf.service.dto.*;
import com.yd.csf.service.enums.FortuneStatusEnum;
import com.yd.csf.service.model.*;
......@@ -75,6 +80,12 @@ public class FortuneServiceImpl extends ServiceImpl<FortuneMapper, Fortune>
private ApiExchangeRateFeignClient apiExchangeRateFeignClient;
@Resource
private IAgentAccumulatedFycService iAgentAccumulatedFycService;
@Resource
private AuditService auditService;
@Resource
private AuditHelper auditHelper;
@Resource
private AuditTemplate auditTemplate;
@Override
public Wrapper<Fortune> getQueryWrapper(FortuneQueryRequest fortuneQueryRequest) {
......@@ -767,6 +778,11 @@ public class FortuneServiceImpl extends ServiceImpl<FortuneMapper, Fortune>
return false;
}
return auditTemplate.execute("出账管理", 1, "批量新增出账记录", ctx ->
doAddFortuneBatch(fortuneAddRequestList, ctx));
}
private Boolean doAddFortuneBatch(List<FortuneAddRequest> fortuneAddRequestList, AuditContext ctx) {
Set<String> policyNoSet = fortuneAddRequestList.stream()
.map(FortuneAddRequest::getPolicyNo)
.filter(StringUtils::isNotBlank).collect(Collectors.toSet());
......@@ -810,6 +826,8 @@ public class FortuneServiceImpl extends ServiceImpl<FortuneMapper, Fortune>
String realName = currentLoginUser.getRealName();
List<Fortune> fortuneList = new ArrayList<>();
// 收集自动创建的 ExpectedFortune bizId
List<String> createdExpectedBizIds = new ArrayList<>();
for (FortuneAddRequest fortuneAddRequest : fortuneAddRequestList) {
// 校验参数
......@@ -845,6 +863,8 @@ public class FortuneServiceImpl extends ServiceImpl<FortuneMapper, Fortune>
.update();
// 关联应付款编号
fortune.setPayableNo(payableNo);
// 记录自动创建的 expectedFortune
createdExpectedBizIds.add(expectedFortune.getExpectedFortuneBizId());
} else {
// 如果有预计出账记录,提示去应付款管理页面修改金额,而不是在出账页面新增
throw new BusinessException(ResultCode.FAIL.getCode(), fortuneAddRequest.getFortuneName() + " 已存在应付记录,请在应付款管理页面修改金额");
......@@ -888,6 +908,25 @@ public class FortuneServiceImpl extends ServiceImpl<FortuneMapper, Fortune>
}
this.saveBatch(fortuneList);
// 审计:fortune INSERT
if (CollUtil.isNotEmpty(fortuneList)) {
Map<String, Fortune> insertMap = fortuneList.stream()
.collect(Collectors.toMap(Fortune::getFortuneBizId, Function.identity()));
auditHelper.record("fortune", null, insertMap, ctx);
}
// 审计:expected_fortune INSERT(自动创建时)
if (CollUtil.isNotEmpty(createdExpectedBizIds)) {
List<ExpectedFortune> createdList = expectedFortuneService.lambdaQuery()
.in(ExpectedFortune::getExpectedFortuneBizId, createdExpectedBizIds)
.list();
if (CollUtil.isNotEmpty(createdList)) {
Map<String, ExpectedFortune> efInsertMap = createdList.stream()
.collect(Collectors.toMap(ExpectedFortune::getExpectedFortuneBizId, Function.identity()));
auditHelper.record("expected_fortune", null, efInsertMap, ctx);
}
}
return true;
}
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yd.csf.service.dao.AuditLogDataChangeMapper">
<resultMap id="BaseResultMap" type="com.yd.csf.service.model.SysOperDataChange">
<id property="id" column="id"/>
<result property="requestId" column="request_id"/>
<result property="operLogId" column="oper_log_id"/>
<result property="tableName" column="table_name"/>
<result property="recordId" column="record_id"/>
<result property="fieldName" column="field_name"/>
<result property="fieldComment" column="field_comment"/>
<result property="oldValue" column="old_value"/>
<result property="newValue" column="new_value"/>
<result property="changeType" column="change_type"/>
<result property="createTime" column="create_time"/>
</resultMap>
<sql id="Base_Column_List">
id
,request_id,oper_log_id,table_name,record_id,field_name,
field_comment,old_value,new_value,change_type,create_time
</sql>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yd.csf.service.dao.AuditLogDataSnapshotMapper">
<resultMap id="BaseResultMap" type="com.yd.csf.service.model.SysOperDataSnapshot">
<id property="id" column="id"/>
<result property="requestId" column="request_id"/>
<result property="tableName" column="table_name"/>
<result property="recordId" column="record_id"/>
<result property="beforeJson" column="before_json"/>
<result property="afterJson" column="after_json"/>
<result property="createTime" column="create_time"/>
</resultMap>
<sql id="Base_Column_List">
id
,request_id,table_name,record_id,before_json,after_json,
create_time
</sql>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yd.csf.service.dao.AuditLogMapper">
<resultMap id="BaseResultMap" type="com.yd.csf.service.model.SysOperLog">
<id property="id" column="id"/>
<result property="requestId" column="request_id"/>
<result property="module" column="module"/>
<result property="businessType" column="business_type"/>
<result property="method" column="method"/>
<result property="content" column="content"/>
<result property="businessId" column="business_id"/>
<result property="operationType" column="operation_type"/>
<result property="userId" column="user_id"/>
<result property="operName" column="oper_name"/>
<result property="deptName" column="dept_name"/>
<result property="operUrl" column="oper_url"/>
<result property="operIp" column="oper_ip"/>
<result property="operLocation" column="oper_location"/>
<result property="operParam" column="oper_param"/>
<result property="jsonResult" column="json_result"/>
<result property="status" column="status"/>
<result property="errorMsg" column="error_msg"/>
<result property="operTime" column="oper_time"/>
</resultMap>
<sql id="Base_Column_List">
id
,request_id,module,business_type,method,content,
business_id,operation_type,user_id,oper_name,dept_name,
oper_url,oper_ip,oper_location,oper_param,json_result,
status,error_msg,oper_time
</sql>
</mapper>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment