Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Y
yichengstreet-be
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
yichengstreet
yichengstreet-be
Commits
e6ce768b
Commit
e6ce768b
authored
Apr 22, 2026
by
lixuan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 需求
parent
7d213d86
Pipeline
#147221
failed with stages
in 0 seconds
Changes
22
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
1425 additions
and
1 deletion
+1425
-1
DataFieldChangeRecordController.java
...er/fieldchangerecord/DataFieldChangeRecordController.java
+53
-0
MyBatisConfig.java
...c/main/java/com/ruoyi/framework/config/MyBatisConfig.java
+2
-0
DataFieldChangeLogInterceptor.java
...rk/interceptor/mybatis/DataFieldChangeLogInterceptor.java
+395
-0
DataFieldChangeRecord.java
...ystem/domain/fieldchangerecord/DataFieldChangeRecord.java
+42
-0
DataFieldChangeLog.java
...main/fieldchangerecord/annotation/DataFieldChangeLog.java
+61
-0
DataFieldChangeLogs.java
...ain/fieldchangerecord/annotation/DataFieldChangeLogs.java
+19
-0
FieldCode.java
...uoyi/system/domain/fieldchangerecord/enums/FieldCode.java
+106
-0
FieldValueType.java
...system/domain/fieldchangerecord/enums/FieldValueType.java
+19
-0
FieldChangeRecordQuery.java
...m/domain/fieldchangerecord/vo/FieldChangeRecordQuery.java
+50
-0
FieldChangeRecordVO.java
...stem/domain/fieldchangerecord/vo/FieldChangeRecordVO.java
+63
-0
FieldChangeSummaryRow.java
...em/domain/fieldchangerecord/vo/FieldChangeSummaryRow.java
+24
-0
DataFieldChangeRecordMapper.java
...mapper/fieldchangerecord/DataFieldChangeRecordMapper.java
+20
-0
DataFieldChangeRecordQueryMapper.java
...r/fieldchangerecord/DataFieldChangeRecordQueryMapper.java
+37
-0
BusinessEntityInfoMapper.java
...m/ruoyi/system/mapper/house/BusinessEntityInfoMapper.java
+23
-0
BusinessEntitySellMapper.java
...m/ruoyi/system/mapper/house/BusinessEntitySellMapper.java
+16
-0
HouseResourceMapper.java
...va/com/ruoyi/system/mapper/house/HouseResourceMapper.java
+42
-0
DataFieldChangeRecordService.java
...rvice/fieldchangerecord/DataFieldChangeRecordService.java
+25
-0
DataFieldChangeRecordServiceImpl.java
...ldchangerecord/impl/DataFieldChangeRecordServiceImpl.java
+175
-0
HouseResourceServiceImpl.java
...i/system/service/house/impl/HouseResourceServiceImpl.java
+1
-1
DataFieldChangeRecordMapper.xml
.../mapper/fieldchangerecord/DataFieldChangeRecordMapper.xml
+15
-0
DataFieldChangeRecordQueryMapper.xml
...er/fieldchangerecord/DataFieldChangeRecordQueryMapper.xml
+212
-0
data_field_change_record.sql
sql/data_field_change_record.sql
+25
-0
No files found.
ruoyi-admin/src/main/java/com/ruoyi/web/controller/fieldchangerecord/DataFieldChangeRecordController.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
web
.
controller
.
fieldchangerecord
;
import
com.ruoyi.common.core.domain.AjaxResult
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordQuery
;
import
com.ruoyi.system.service.fieldchangerecord.DataFieldChangeRecordService
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
/**
* 字段级变更统计对外接口。
*
* <p>房源侧和经营主体侧完全分开的 4 个 GET 接口:</p>
* <ul>
* <li>{@code /houseResource}:HR 侧明细分页</li>
* <li>{@code /houseResource/summary}:HR 侧按字段 + 操作的计数</li>
* <li>{@code /businessEntity}:BE 侧明细分页(展开每个关联房源)</li>
* <li>{@code /businessEntity/summary}:BE 侧汇总(按日志去重)</li>
* </ul>
*
* <p>入参形态详见 {@link FieldChangeRecordQuery}。{@code changeStartTime /
* changeEndTime} 必填;{@code wgCodes} 由 Service 层按当前登录人注入,前端不传。</p>
*/
@RestController
@RequestMapping
(
"/api/house/fieldChangeRecords"
)
public
class
DataFieldChangeRecordController
{
private
final
DataFieldChangeRecordService
service
;
public
DataFieldChangeRecordController
(
DataFieldChangeRecordService
service
)
{
this
.
service
=
service
;
}
@GetMapping
(
"/houseResource"
)
public
AjaxResult
pageHouseResource
(
FieldChangeRecordQuery
query
)
{
return
AjaxResult
.
success
(
service
.
pageHouseResourceFieldChanges
(
query
));
}
@GetMapping
(
"/houseResource/summary"
)
public
AjaxResult
summaryHouseResource
(
FieldChangeRecordQuery
query
)
{
return
AjaxResult
.
success
(
service
.
summaryHouseResourceFieldChanges
(
query
));
}
@GetMapping
(
"/businessEntity"
)
public
AjaxResult
pageBusinessEntity
(
FieldChangeRecordQuery
query
)
{
return
AjaxResult
.
success
(
service
.
pageBusinessEntityFieldChanges
(
query
));
}
@GetMapping
(
"/businessEntity/summary"
)
public
AjaxResult
summaryBusinessEntity
(
FieldChangeRecordQuery
query
)
{
return
AjaxResult
.
success
(
service
.
summaryBusinessEntityFieldChanges
(
query
));
}
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
View file @
e6ce768b
...
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
...
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import
com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
;
import
com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
;
import
com.ruoyi.common.utils.StringUtils
;
import
com.ruoyi.common.utils.StringUtils
;
import
com.ruoyi.framework.interceptor.mybatis.DataChangeLogInterceptor
;
import
com.ruoyi.framework.interceptor.mybatis.DataChangeLogInterceptor
;
import
com.ruoyi.framework.interceptor.mybatis.DataFieldChangeLogInterceptor
;
import
org.apache.ibatis.io.VFS
;
import
org.apache.ibatis.io.VFS
;
import
org.apache.ibatis.plugin.Interceptor
;
import
org.apache.ibatis.plugin.Interceptor
;
import
org.apache.ibatis.session.SqlSessionFactory
;
import
org.apache.ibatis.session.SqlSessionFactory
;
...
@@ -127,6 +128,7 @@ public class MyBatisConfig
...
@@ -127,6 +128,7 @@ public class MyBatisConfig
VFS
.
addImplClass
(
SpringBootVFS
.
class
);
VFS
.
addImplClass
(
SpringBootVFS
.
class
);
Interceptor
[]
interceptors
=
{
Interceptor
[]
interceptors
=
{
new
DataChangeLogInterceptor
(),
new
DataChangeLogInterceptor
(),
new
DataFieldChangeLogInterceptor
(),
new
PaginationInterceptor
()
new
PaginationInterceptor
()
};
};
final
MybatisSqlSessionFactoryBean
sessionFactory
=
new
MybatisSqlSessionFactoryBean
();
final
MybatisSqlSessionFactoryBean
sessionFactory
=
new
MybatisSqlSessionFactoryBean
();
...
...
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/mybatis/DataFieldChangeLogInterceptor.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
framework
.
interceptor
.
mybatis
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.fieldchangerecord.DataFieldChangeRecord
;
import
com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
org.apache.ibatis.executor.Executor
;
import
org.apache.ibatis.mapping.MappedStatement
;
import
org.apache.ibatis.plugin.Interceptor
;
import
org.apache.ibatis.plugin.Intercepts
;
import
org.apache.ibatis.plugin.Invocation
;
import
org.apache.ibatis.plugin.Signature
;
import
org.apache.ibatis.session.ResultHandler
;
import
org.apache.ibatis.session.RowBounds
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
java.lang.reflect.Field
;
import
java.lang.reflect.Method
;
import
java.math.BigDecimal
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.Date
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Objects
;
import
java.util.UUID
;
import
java.util.concurrent.ConcurrentHashMap
;
/**
* 字段级变更日志拦截器。
*
* <p>和 {@link DataChangeLogInterceptor} 并行工作,拦截
* {@link Executor#update(MappedStatement, Object)} 调用,读取 Mapper 方法上的
* {@link DataFieldChangeLog} 注解,对声明的字段做前后值 diff 后写入
* {@code data_field_change_record} 表。</p>
*
* <p>关键约束:</p>
* <ol>
* <li>只处理 {@link OperationType#INSERT} 和 {@link OperationType#UPDATE},
* DELETE 直接跳过(继续走主体级日志)。</li>
* <li>UPDATE 场景必须配置 {@code preLoadMapper} 捞旧记录;否则无法 diff 直接跳过。</li>
* <li><strong>Partial update 语义</strong>:入参里字段值为 null 视为"未修改",不 diff、不写日志。</li>
* <li>受影响行数为 0 时跳过日志写入。</li>
* <li>INSERT 且字段值为 null 时不写日志(避免一条 insert 炸出 N 条空日志)。</li>
* <li>{@link FieldCode#BE_YEAR_SELL} 的值格式化为 {@code "year=2025;value=523.50"}。</li>
* <li>BigDecimal 比较采用 {@code compareTo},避免 scale 不同导致误判(1.0 vs 1.00)。</li>
* <li>日志写入失败抛出异常,业务事务整体回滚。</li>
* </ol>
*/
@Intercepts
({
@Signature
(
type
=
Executor
.
class
,
method
=
"update"
,
args
=
{
MappedStatement
.
class
,
Object
.
class
})
})
public
class
DataFieldChangeLogInterceptor
implements
Interceptor
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
DataFieldChangeLogInterceptor
.
class
);
private
static
final
String
LOG_INSERT_STATEMENT_ID
=
"com.ruoyi.system.mapper.fieldchangerecord.DataFieldChangeRecordMapper.batchInsert"
;
private
static
final
DataFieldChangeLog
[]
EMPTY
=
new
DataFieldChangeLog
[
0
];
/** 按 MappedStatement id 缓存注解列表。 */
private
final
Map
<
String
,
DataFieldChangeLog
[]>
annotationCache
=
new
ConcurrentHashMap
<>();
@Override
public
Object
intercept
(
Invocation
invocation
)
throws
Throwable
{
Object
[]
args
=
invocation
.
getArgs
();
MappedStatement
ms
=
(
MappedStatement
)
args
[
0
];
Object
param
=
args
[
1
];
DataFieldChangeLog
[]
annotations
=
resolveAnnotations
(
ms
.
getId
());
if
(
annotations
.
length
==
0
)
{
return
invocation
.
proceed
();
}
Executor
executor
=
(
Executor
)
invocation
.
getTarget
();
// UPDATE 场景先捞旧值,按 preLoadMapper 缓存避免重复查询。
Map
<
String
,
Map
<
String
,
Object
>>
preloadByMapper
=
runPreload
(
annotations
,
param
,
executor
,
ms
);
Object
result
=
invocation
.
proceed
();
if
(
result
instanceof
Integer
&&
(
Integer
)
result
==
0
)
{
return
result
;
}
List
<
DataFieldChangeRecord
>
records
=
buildRecords
(
annotations
,
param
,
preloadByMapper
);
if
(
records
.
isEmpty
())
{
return
result
;
}
try
{
MappedStatement
logMs
=
ms
.
getConfiguration
().
getMappedStatement
(
LOG_INSERT_STATEMENT_ID
);
Map
<
String
,
Object
>
logParam
=
new
HashMap
<>(
2
);
logParam
.
put
(
"list"
,
records
);
executor
.
update
(
logMs
,
logParam
);
}
catch
(
Exception
e
)
{
log
.
error
(
"写入 data_field_change_record 失败,业务事务将回滚。ms={}"
,
ms
.
getId
(),
e
);
throw
e
;
}
return
result
;
}
@Override
public
Object
plugin
(
Object
target
)
{
if
(
target
instanceof
Executor
)
{
return
org
.
apache
.
ibatis
.
plugin
.
Plugin
.
wrap
(
target
,
this
);
}
return
target
;
}
@Override
public
void
setProperties
(
java
.
util
.
Properties
properties
)
{
}
private
DataFieldChangeLog
[]
resolveAnnotations
(
String
statementId
)
{
return
annotationCache
.
computeIfAbsent
(
statementId
,
this
::
loadAnnotations
);
}
private
DataFieldChangeLog
[]
loadAnnotations
(
String
statementId
)
{
int
dot
=
statementId
.
lastIndexOf
(
'.'
);
if
(
dot
<=
0
)
{
return
EMPTY
;
}
String
mapperClassName
=
statementId
.
substring
(
0
,
dot
);
String
methodName
=
statementId
.
substring
(
dot
+
1
);
try
{
Class
<?>
mapperClass
=
Class
.
forName
(
mapperClassName
);
for
(
Method
m
:
mapperClass
.
getDeclaredMethods
())
{
if
(!
m
.
getName
().
equals
(
methodName
))
{
continue
;
}
DataFieldChangeLog
[]
annos
=
m
.
getAnnotationsByType
(
DataFieldChangeLog
.
class
);
if
(
annos
.
length
>
0
)
{
return
annos
;
}
}
}
catch
(
ClassNotFoundException
ignore
)
{
}
return
EMPTY
;
}
/**
* 对所有 UPDATE 注解执行 preload 查询,返回 {@code preLoadMapperFullName -> pk -> oldEntity} 三层结构。
* 同一 preLoadMapper 只查一次。
*/
private
Map
<
String
,
Map
<
String
,
Object
>>
runPreload
(
DataFieldChangeLog
[]
annotations
,
Object
param
,
Executor
executor
,
MappedStatement
ms
)
throws
Exception
{
Map
<
String
,
Map
<
String
,
Object
>>
results
=
new
HashMap
<>();
for
(
DataFieldChangeLog
anno
:
annotations
)
{
if
(
anno
.
operation
()
!=
OperationType
.
UPDATE
)
{
continue
;
}
String
preLoad
=
anno
.
preLoadMapper
();
if
(
preLoad
.
isEmpty
()
||
results
.
containsKey
(
preLoad
))
{
continue
;
}
MappedStatement
preLoadMs
=
ms
.
getConfiguration
().
getMappedStatement
(
preLoad
);
@SuppressWarnings
(
"rawtypes"
)
List
queryResult
=
executor
.
query
(
preLoadMs
,
param
,
RowBounds
.
DEFAULT
,
(
ResultHandler
)
null
);
Map
<
String
,
Object
>
byPk
=
new
HashMap
<>();
if
(
queryResult
!=
null
)
{
for
(
Object
old
:
queryResult
)
{
if
(
old
==
null
)
{
continue
;
}
Object
pk
=
getProperty
(
old
,
"id"
);
if
(
pk
!=
null
)
{
byPk
.
put
(
String
.
valueOf
(
pk
),
old
);
}
}
}
results
.
put
(
preLoad
,
byPk
);
}
return
results
;
}
private
List
<
DataFieldChangeRecord
>
buildRecords
(
DataFieldChangeLog
[]
annotations
,
Object
param
,
Map
<
String
,
Map
<
String
,
Object
>>
preloadByMapper
)
{
Date
now
=
new
Date
();
List
<
DataFieldChangeRecord
>
records
=
new
ArrayList
<>();
for
(
DataFieldChangeLog
anno
:
annotations
)
{
if
(
anno
.
operation
()
!=
OperationType
.
INSERT
&&
anno
.
operation
()
!=
OperationType
.
UPDATE
)
{
continue
;
}
// UPDATE 必须配 preLoadMapper,否则无法 diff。
Map
<
String
,
Object
>
oldByPk
=
Collections
.
emptyMap
();
if
(
anno
.
operation
()
==
OperationType
.
UPDATE
)
{
if
(
anno
.
preLoadMapper
().
isEmpty
())
{
log
.
warn
(
"@DataFieldChangeLog UPDATE 未配 preLoadMapper,跳过 diff。subjectType={}"
,
anno
.
subjectType
());
continue
;
}
oldByPk
=
preloadByMapper
.
getOrDefault
(
anno
.
preLoadMapper
(),
Collections
.
emptyMap
());
}
List
<
Subject
>
subjects
=
resolveSubjects
(
param
,
anno
.
idExpr
(),
oldByPk
);
for
(
Subject
subject
:
subjects
)
{
if
(
subject
.
subjectId
==
null
||
subject
.
subjectId
.
isEmpty
()
||
subject
.
source
==
null
)
{
continue
;
}
Object
sourcePk
=
getProperty
(
subject
.
source
,
"id"
);
Object
oldSnapshot
=
sourcePk
==
null
?
null
:
oldByPk
.
get
(
String
.
valueOf
(
sourcePk
));
for
(
FieldCode
field
:
anno
.
fields
())
{
DataFieldChangeRecord
rec
=
buildOneRecord
(
anno
,
subject
,
oldSnapshot
,
field
,
now
);
if
(
rec
!=
null
)
{
records
.
add
(
rec
);
}
}
}
}
return
records
;
}
private
DataFieldChangeRecord
buildOneRecord
(
DataFieldChangeLog
anno
,
Subject
subject
,
Object
oldSnapshot
,
FieldCode
field
,
Date
now
)
{
Object
newRaw
=
getProperty
(
subject
.
source
,
field
.
getPropertyName
());
Object
oldRaw
=
oldSnapshot
==
null
?
null
:
getProperty
(
oldSnapshot
,
field
.
getPropertyName
());
if
(
anno
.
operation
()
==
OperationType
.
INSERT
)
{
if
(
newRaw
==
null
)
{
return
null
;
}
}
else
{
// UPDATE:partial update 语义——newRaw 为 null 视为未修改。
if
(
newRaw
==
null
)
{
return
null
;
}
if
(
valuesEqual
(
newRaw
,
oldRaw
))
{
return
null
;
}
}
String
oldStr
;
String
newStr
;
if
(
field
==
FieldCode
.
BE_YEAR_SELL
)
{
Object
newYear
=
getProperty
(
subject
.
source
,
"year"
);
Object
oldYear
=
oldSnapshot
==
null
?
null
:
getProperty
(
oldSnapshot
,
"year"
);
newStr
=
formatYearSell
(
newYear
,
newRaw
);
oldStr
=
(
oldSnapshot
==
null
||
oldRaw
==
null
)
?
null
:
formatYearSell
(
oldYear
,
oldRaw
);
}
else
{
newStr
=
String
.
valueOf
(
newRaw
);
oldStr
=
oldRaw
==
null
?
null
:
String
.
valueOf
(
oldRaw
);
}
DataFieldChangeRecord
r
=
new
DataFieldChangeRecord
();
r
.
setId
(
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
r
.
setSubjectType
(
anno
.
subjectType
().
name
());
r
.
setSubjectId
(
subject
.
subjectId
);
r
.
setFieldCode
(
field
.
name
());
r
.
setOldValue
(
oldStr
);
r
.
setNewValue
(
newStr
);
r
.
setOperationType
(
anno
.
operation
().
getCode
());
r
.
setOperationTime
(
now
);
return
r
;
}
private
String
formatYearSell
(
Object
year
,
Object
value
)
{
return
"year="
+
(
year
==
null
?
""
:
String
.
valueOf
(
year
))
+
";value="
+
(
value
==
null
?
""
:
String
.
valueOf
(
value
));
}
/**
* 解析 idExpr,返回 (subjectId, 值源对象) 列表。
*
* <p>值源对象用于后续反射取字段新值。对于 {@code list[*].xxx} 批量表达式,
* 每个列表元素分别作为独立的值源。</p>
*
* <p>特殊:{@code @preload[*].xxx} 用于 partial update 场景(例如
* {@code updateBusinessEntitySell} 入参不含 {@code businessEntityInfoId} 时,
* 需要从预加载的旧记录拿 subject_id)。此时 source 仍是 {@code param},因为字段
* 新值必然来自业务写操作的入参,而 pk 匹配依然能将 source 和 oldSnapshot 挂钩。
* 该特殊路径只适用于"单实体 update"场景(preload 结果大小为 1)。</p>
*/
private
List
<
Subject
>
resolveSubjects
(
Object
param
,
String
idExpr
,
Map
<
String
,
Object
>
oldByPk
)
{
if
(
idExpr
==
null
||
idExpr
.
isEmpty
()
||
param
==
null
)
{
return
Collections
.
emptyList
();
}
int
starIdx
=
idExpr
.
indexOf
(
"[*]"
);
if
(
starIdx
>=
0
)
{
String
listKey
=
idExpr
.
substring
(
0
,
starIdx
);
String
afterStar
=
idExpr
.
substring
(
starIdx
+
3
);
if
(
afterStar
.
startsWith
(
"."
))
{
afterStar
=
afterStar
.
substring
(
1
);
}
if
(
"@preload"
.
equals
(
listKey
))
{
if
(
oldByPk
==
null
||
oldByPk
.
isEmpty
())
{
return
Collections
.
emptyList
();
}
List
<
Subject
>
subjects
=
new
ArrayList
<>(
oldByPk
.
size
());
for
(
Object
old
:
oldByPk
.
values
())
{
Object
idVal
=
afterStar
.
isEmpty
()
?
old
:
getProperty
(
old
,
afterStar
);
if
(
idVal
==
null
)
{
continue
;
}
subjects
.
add
(
new
Subject
(
String
.
valueOf
(
idVal
),
param
));
}
return
subjects
;
}
Object
v
=
getProperty
(
param
,
listKey
);
if
(!(
v
instanceof
List
))
{
return
Collections
.
emptyList
();
}
List
<?>
list
=
(
List
<?>)
v
;
List
<
Subject
>
subjects
=
new
ArrayList
<>(
list
.
size
());
for
(
Object
item
:
list
)
{
if
(
item
==
null
)
{
continue
;
}
Object
idVal
=
afterStar
.
isEmpty
()
?
item
:
getProperty
(
item
,
afterStar
);
if
(
idVal
==
null
)
{
continue
;
}
subjects
.
add
(
new
Subject
(
String
.
valueOf
(
idVal
),
item
));
}
return
subjects
;
}
Object
val
=
getProperty
(
param
,
idExpr
);
if
(
val
==
null
)
{
return
Collections
.
emptyList
();
}
return
Collections
.
singletonList
(
new
Subject
(
String
.
valueOf
(
val
),
param
));
}
private
boolean
valuesEqual
(
Object
a
,
Object
b
)
{
if
(
a
==
null
&&
b
==
null
)
{
return
true
;
}
if
(
a
==
null
||
b
==
null
)
{
return
false
;
}
if
(
a
instanceof
BigDecimal
&&
b
instanceof
BigDecimal
)
{
return
((
BigDecimal
)
a
).
compareTo
((
BigDecimal
)
b
)
==
0
;
}
return
Objects
.
equals
(
a
,
b
);
}
private
Object
getProperty
(
Object
obj
,
String
key
)
{
if
(
obj
==
null
||
key
==
null
||
key
.
isEmpty
())
{
return
null
;
}
if
(
obj
instanceof
Map
)
{
return
((
Map
<?,
?>)
obj
).
get
(
key
);
}
try
{
String
capitalized
=
Character
.
toUpperCase
(
key
.
charAt
(
0
))
+
key
.
substring
(
1
);
String
getter
=
"get"
+
capitalized
;
String
booleanGetter
=
"is"
+
capitalized
;
for
(
Method
m
:
obj
.
getClass
().
getMethods
())
{
if
(
m
.
getParameterCount
()
!=
0
)
{
continue
;
}
if
(
m
.
getName
().
equals
(
getter
)
||
m
.
getName
().
equals
(
booleanGetter
))
{
return
m
.
invoke
(
obj
);
}
}
Field
f
=
findField
(
obj
.
getClass
(),
key
);
if
(
f
!=
null
)
{
f
.
setAccessible
(
true
);
return
f
.
get
(
obj
);
}
}
catch
(
Exception
ignore
)
{
}
return
null
;
}
private
Field
findField
(
Class
<?>
cls
,
String
name
)
{
Class
<?>
c
=
cls
;
while
(
c
!=
null
&&
c
!=
Object
.
class
)
{
for
(
Field
f
:
c
.
getDeclaredFields
())
{
if
(
f
.
getName
().
equals
(
name
))
{
return
f
;
}
}
c
=
c
.
getSuperclass
();
}
return
null
;
}
/** 一个 (subject_id, 值源对象) 二元组。 */
private
static
final
class
Subject
{
final
String
subjectId
;
final
Object
source
;
Subject
(
String
subjectId
,
Object
source
)
{
this
.
subjectId
=
subjectId
;
this
.
source
=
source
;
}
}
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/DataFieldChangeRecord.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
;
import
lombok.Data
;
import
java.io.Serializable
;
import
java.util.Date
;
/**
* 字段级变更记录。
*
* <p>对应数据库表 {@code data_field_change_record}。</p>
*
* <ul>
* <li>{@link #subjectType} 取值见 {@link com.ruoyi.system.domain.changerecord.enums.SubjectType}。</li>
* <li>{@link #fieldCode} 取值见
* {@link com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode}。</li>
* <li>{@link #operationType} 仅会是 1(INSERT)或 2(UPDATE);DELETE 不进本表。</li>
* <li>{@link #oldValue} / {@link #newValue} 对于图片字段直接存真实 URL;对于
* {@code BE_YEAR_SELL} 存 {@code "year=2025;value=523.50"} 形式。</li>
* </ul>
*/
@Data
public
class
DataFieldChangeRecord
implements
Serializable
{
private
static
final
long
serialVersionUID
=
1L
;
private
String
id
;
private
String
subjectType
;
private
String
subjectId
;
private
String
fieldCode
;
private
String
oldValue
;
private
String
newValue
;
private
Integer
operationType
;
private
Date
operationTime
;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/annotation/DataFieldChangeLog.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
annotation
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
java.lang.annotation.ElementType
;
import
java.lang.annotation.Repeatable
;
import
java.lang.annotation.Retention
;
import
java.lang.annotation.RetentionPolicy
;
import
java.lang.annotation.Target
;
/**
* 字段级变更日志注解。
*
* <p>标注在 Mapper 方法上,用于声明该写操作需要对指定 {@link #fields()} 做前后值 diff,
* 并将真变更写入 {@code data_field_change_record} 表。和主体级 {@code @DataChangeLog}
* 并行存在,互不影响。</p>
*
* <h3>idExpr 表达式语法</h3>
* <p>与 {@code @DataChangeLog} 完全一致:</p>
* <ul>
* <li>{@code "fieldName"} — 从参数按属性名取值;</li>
* <li>{@code "list[*].fieldName"} — 批量操作场景;</li>
* <li>{@code "@preload[*].fieldName"} — 从 {@link #preLoadMapper()} 查询结果中取值。</li>
* </ul>
*
* <h3>preLoadMapper</h3>
* <p><strong>UPDATE 场景必需</strong>:diff 需要知道旧值,拦截器会在执行业务 SQL 前
* 先调用 preLoadMapper 查出旧记录。INSERT 场景 preLoadMapper 留空即可。</p>
*
* <h3>Partial update 语义</h3>
* <p>UPDATE 时,如果 new value(从参数里取)为 {@code null},拦截器会跳过该字段 diff
* 不产生日志(视为"未修改"),兼容 MyBatis {@code <set>} 标签只拼非 null 字段的写法。</p>
*/
@Target
(
ElementType
.
METHOD
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Repeatable
(
DataFieldChangeLogs
.
class
)
public
@interface
DataFieldChangeLog
{
/** 主体类型。必须和 {@link #fields()} 里每个 FieldCode 的 subjectType 一致。 */
SubjectType
subjectType
();
/** 操作类型。仅支持 INSERT 和 UPDATE;DELETE 不进本表。 */
OperationType
operation
();
/** subject_id 取值表达式。 */
String
idExpr
();
/** 本次需要 diff 的字段清单。 */
FieldCode
[]
fields
();
/**
* UPDATE 场景下,用于捞旧记录的查询 Mapper 方法全限定名。
* INSERT 场景留空。
*
* <p>预加载的返回值应该是与主体同构的对象列表(每个元素是 HouseResource /
* BusinessEntityInfo / BusinessEntitySell 等),拦截器会按 id 匹配找到对应旧值。</p>
*/
String
preLoadMapper
()
default
""
;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/annotation/DataFieldChangeLogs.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
annotation
;
import
java.lang.annotation.ElementType
;
import
java.lang.annotation.Retention
;
import
java.lang.annotation.RetentionPolicy
;
import
java.lang.annotation.Target
;
/**
* {@link DataFieldChangeLog} 的容器注解。
*
* <p>通过 {@code @Repeatable} 机制,在同一个方法上写多个
* {@link DataFieldChangeLog} 会被自动包装成此容器注解。</p>
*/
@Target
(
ElementType
.
METHOD
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
@interface
DataFieldChangeLogs
{
DataFieldChangeLog
[]
value
();
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/enums/FieldCode.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
enums
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
/**
* 字段级变更的业务语义字段码。
*
* <p>一共 10 个字段,覆盖需求方指定的全部统计维度:</p>
* <ul>
* <li>HR 侧 6 个:业务场所状态 / 面积 / 产权人联系方式 / 租金单价 / 场所图片 / 营业执照图片</li>
* <li>BE 侧 4 个:名称 / 负责人联系方式 / 工作人员数量 / 年度销售额</li>
* </ul>
*
* <p>{@link #propertyName} 是对应 Java 实体 (HouseResource / BusinessEntityInfo /
* BusinessEntitySell) 上的属性名,拦截器据此反射取值做 diff。</p>
*
* <p>{@code BE_YEAR_SELL} 较特殊:subject_id 取 {@code business_entity_info_id},
* old / new value 序列化为 {@code year=2025;value=523.50} 形式,拦截器在 diff 时按
* 这个格式组装。</p>
*/
public
enum
FieldCode
{
// --- BUSINESS_ENTITY_INFO ---
BE_NAME
(
SubjectType
.
BUSINESS_ENTITY_INFO
,
FieldValueType
.
TEXT
,
"name"
,
"经营主体名称"
),
BE_PRINCIPAL_TEL
(
SubjectType
.
BUSINESS_ENTITY_INFO
,
FieldValueType
.
TEXT
,
"principalTel"
,
"负责人联系方式"
),
BE_WORKER_NUMBER
(
SubjectType
.
BUSINESS_ENTITY_INFO
,
FieldValueType
.
NUMBER
,
"workerNumber"
,
"工作人员数量"
),
/**
* 年度销售额。subject 挂在 BUSINESS_ENTITY_INFO 上,值以
* {@code year=2025;value=523.50} 形式存储。
*/
BE_YEAR_SELL
(
SubjectType
.
BUSINESS_ENTITY_INFO
,
FieldValueType
.
NUMBER
,
"yearSell"
,
"年度销售额"
),
// --- HOUSE_RESOURCE ---
HR_BUSINESS_STATUS
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
ENUM
,
"businessStatus"
,
"经营场所状态"
),
HR_AREA
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
NUMBER
,
"houseArea"
,
"经营场所面积"
),
HR_EQUITY_TEL
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
TEXT
,
"houseResourceEquityTel"
,
"产权人联系方式"
),
HR_UNIT_PRICE
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
NUMBER
,
"unitPrice"
,
"租金"
),
HR_IMAGE
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
IMAGE
,
"houseResourceUrl"
,
"经营场所图片"
),
HR_LICENSE_IMAGE
(
SubjectType
.
HOUSE_RESOURCE
,
FieldValueType
.
IMAGE
,
"businessLicenseUrl"
,
"营业执照图片"
);
private
final
SubjectType
subjectType
;
private
final
FieldValueType
valueType
;
/** 对应 Java 实体上的属性名。拦截器据此反射取值。 */
private
final
String
propertyName
;
/** 中文 label,接口层返回给前端展示。 */
private
final
String
label
;
FieldCode
(
SubjectType
subjectType
,
FieldValueType
valueType
,
String
propertyName
,
String
label
)
{
this
.
subjectType
=
subjectType
;
this
.
valueType
=
valueType
;
this
.
propertyName
=
propertyName
;
this
.
label
=
label
;
}
public
SubjectType
getSubjectType
()
{
return
subjectType
;
}
public
FieldValueType
getValueType
()
{
return
valueType
;
}
public
String
getPropertyName
()
{
return
propertyName
;
}
public
String
getLabel
()
{
return
label
;
}
/** 根据 code 字符串反查枚举;未知 code 返回 null(不抛异常,便于前端容错)。 */
public
static
FieldCode
fromCode
(
String
code
)
{
if
(
code
==
null
)
{
return
null
;
}
for
(
FieldCode
f
:
values
())
{
if
(
f
.
name
().
equals
(
code
))
{
return
f
;
}
}
return
null
;
}
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/enums/FieldValueType.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
enums
;
/**
* 字段值的展示类型,前端据此选择渲染分支。
*/
public
enum
FieldValueType
{
/** 纯文本,前端直接展示 "老值 -> 新值"。 */
TEXT
,
/** 枚举,前端需要字典翻译后再展示。 */
ENUM
,
/** 数值。 */
NUMBER
,
/** 图片 URL(可能是逗号分隔多图),前端渲染缩略图对比。 */
IMAGE
;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/vo/FieldChangeRecordQuery.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
vo
;
import
lombok.Data
;
import
org.springframework.format.annotation.DateTimeFormat
;
import
java.util.Date
;
import
java.util.List
;
/**
* 字段级变更 明细 / 汇总 查询入参。
*
* <p>房源侧和经营主体侧共用此结构,在 Service 层由不同方法分流:</p>
* <ul>
* <li>房源侧:{@code hrTypes / two / three / four} 作用在 {@code house_resource} 自身;</li>
* <li>经营主体侧:通过 mapping → house_resource 关联过滤,即"至少关联到一个匹配条件房源"
* 的经营主体才会被计入。</li>
* </ul>
*/
@Data
public
class
FieldChangeRecordQuery
{
@DateTimeFormat
(
pattern
=
"yyyy-MM-dd HH:mm:ss"
)
private
Date
changeStartTime
;
@DateTimeFormat
(
pattern
=
"yyyy-MM-dd HH:mm:ss"
)
private
Date
changeEndTime
;
/** 字段码多选。为空表示不限(后端默认带入当前主体侧全部字段)。 */
private
List
<
String
>
fieldCodes
;
/** 操作类型多选,取值 1(新增)/ 2(修改)。为空表示不限。 */
private
List
<
Integer
>
operationTypes
;
/** 房源类型多选 1/4/5/6/7。 */
private
List
<
Integer
>
hrTypes
;
private
String
two
;
private
String
three
;
private
String
four
;
/** 当前登录人网格权限范围,由 Service 层注入,前端无需传。 */
private
List
<
String
>
wgCodes
;
/** 明细分页参数;汇总接口忽略。 */
private
Integer
pageNum
;
private
Integer
pageSize
;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/vo/FieldChangeRecordVO.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
vo
;
import
lombok.Data
;
import
java.util.Date
;
/**
* 字段级变更明细返回结构。
*
* <p>Mapper XML 已经把所需的 JOIN 结果(关联房源的 type / 网格 / 地址)铺平到此 VO,
* 前端直接使用。</p>
*/
@Data
public
class
FieldChangeRecordVO
{
/** 日志记录 id。 */
private
String
id
;
/** HOUSE_RESOURCE / BUSINESS_ENTITY_INFO。 */
private
String
subjectType
;
/** 主体 id(房源 id 或经营主体 id)。 */
private
String
subjectId
;
/** 主体展示名:HR 侧为"地址 + 房号"拼接;BE 侧为经营主体 name。 */
private
String
subjectName
;
/** 字段码。 */
private
String
fieldCode
;
/** 字段中文 label,由 Service 层填充(从 FieldCode 枚举读)。 */
private
String
fieldLabel
;
/** 值类型,TEXT/ENUM/NUMBER/IMAGE,由 Service 层填充。 */
private
String
valueType
;
private
String
oldValue
;
private
String
newValue
;
/** 1 新增 / 2 修改。 */
private
Integer
operationType
;
private
Date
operationTime
;
/**
* 关联房源 type(BE 侧一条日志可能对应多行,每行对应一个关联房源)。
* HR 侧就是主体自己的 type。
*/
private
Integer
hrType
;
private
String
two
;
private
String
three
;
private
String
four
;
/** 关联房源 id(BE 侧用于 drilldown)。HR 侧等于 subjectId。 */
private
String
associatedHouseResourceId
;
/** 关联房源展示名(地址 + 房号)。HR 侧与 subjectName 重复,可前端忽略。 */
private
String
associatedHouseResourceName
;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/fieldchangerecord/vo/FieldChangeSummaryRow.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
domain
.
fieldchangerecord
.
vo
;
import
lombok.Data
;
/**
* 字段级变更汇总一行。
*
* <p>返回结构:按 (fieldCode, operationType) 分组的计数。Service 层在此基础上补齐
* 中文 {@code label} 和 {@code valueType}。</p>
*/
@Data
public
class
FieldChangeSummaryRow
{
private
String
fieldCode
;
private
String
fieldLabel
;
private
String
valueType
;
/** 1 新增 / 2 修改。 */
private
Integer
operationType
;
private
Long
count
;
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/fieldchangerecord/DataFieldChangeRecordMapper.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
mapper
.
fieldchangerecord
;
import
com.ruoyi.system.domain.fieldchangerecord.DataFieldChangeRecord
;
import
org.apache.ibatis.annotations.Param
;
import
java.util.List
;
/**
* 字段级变更记录 Mapper。
*/
public
interface
DataFieldChangeRecordMapper
{
/**
* 批量插入字段级变更记录。
*
* <p>本方法不要标注 {@link com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog},
* 否则会无限递归。</p>
*/
int
batchInsert
(
@Param
(
"list"
)
List
<
DataFieldChangeRecord
>
list
);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/fieldchangerecord/DataFieldChangeRecordQueryMapper.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
mapper
.
fieldchangerecord
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordQuery
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordVO
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeSummaryRow
;
import
org.apache.ibatis.annotations.Param
;
import
java.util.List
;
/**
* 字段级变更查询 Mapper。
*
* <p>和 {@link DataFieldChangeRecordMapper}(只负责 batchInsert)分开,职责清晰:
* 写入用拦截器走 batchInsert;读取由此 Mapper 承担,包含 HR 侧和 BE 侧两条
* 完全独立的查询路径。</p>
*/
public
interface
DataFieldChangeRecordQueryMapper
{
/** HR 侧明细,分页。走 data_field_change_record JOIN house_resource。 */
IPage
<
FieldChangeRecordVO
>
selectHouseResourceFieldChanges
(
IPage
<
FieldChangeRecordVO
>
page
,
@Param
(
"query"
)
FieldChangeRecordQuery
query
);
/** HR 侧汇总,按 (fieldCode, operationType) 分组。 */
List
<
FieldChangeSummaryRow
>
selectHouseResourceFieldChangeSummary
(
@Param
(
"query"
)
FieldChangeRecordQuery
query
);
/** BE 侧明细,分页。走 data_field_change_record JOIN bei + mapping + hr。 */
IPage
<
FieldChangeRecordVO
>
selectBusinessEntityFieldChanges
(
IPage
<
FieldChangeRecordVO
>
page
,
@Param
(
"query"
)
FieldChangeRecordQuery
query
);
/** BE 侧汇总。为避免 JOIN 膨胀,对日志 id 做 distinct 计数。 */
List
<
FieldChangeSummaryRow
>
selectBusinessEntityFieldChangeSummary
(
@Param
(
"query"
)
FieldChangeRecordQuery
query
);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/house/BusinessEntityInfoMapper.java
View file @
e6ce768b
...
@@ -3,6 +3,8 @@ package com.ruoyi.system.mapper.house;
...
@@ -3,6 +3,8 @@ package com.ruoyi.system.mapper.house;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
com.ruoyi.system.domain.house.BusinessEntityInfo
;
import
com.ruoyi.system.domain.house.BusinessEntityInfo
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Param
;
...
@@ -13,6 +15,16 @@ public interface BusinessEntityInfoMapper {
...
@@ -13,6 +15,16 @@ public interface BusinessEntityInfoMapper {
// 插入业务实体信息
// 插入业务实体信息
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
,
fields
=
{
FieldCode
.
BE_NAME
,
FieldCode
.
BE_PRINCIPAL_TEL
,
FieldCode
.
BE_WORKER_NUMBER
}
)
void
insertBusinessEntityInfo
(
BusinessEntityInfo
businessEntityInfo
);
void
insertBusinessEntityInfo
(
BusinessEntityInfo
businessEntityInfo
);
// 根据ID查询业务实体信息
// 根据ID查询业务实体信息
...
@@ -20,6 +32,17 @@ public interface BusinessEntityInfoMapper {
...
@@ -20,6 +32,17 @@ public interface BusinessEntityInfoMapper {
// 更新业务实体信息
// 更新业务实体信息
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
,
preLoadMapper
=
"com.ruoyi.system.mapper.house.BusinessEntityInfoMapper.selectBusinessEntityInfoById"
,
fields
=
{
FieldCode
.
BE_NAME
,
FieldCode
.
BE_PRINCIPAL_TEL
,
FieldCode
.
BE_WORKER_NUMBER
}
)
void
updateBusinessEntityInfo
(
BusinessEntityInfo
businessEntityInfo
);
void
updateBusinessEntityInfo
(
BusinessEntityInfo
businessEntityInfo
);
// 删除业务实体信息
// 删除业务实体信息
...
...
ruoyi-system/src/main/java/com/ruoyi/system/mapper/house/BusinessEntitySellMapper.java
View file @
e6ce768b
...
@@ -3,6 +3,8 @@ package com.ruoyi.system.mapper.house;
...
@@ -3,6 +3,8 @@ package com.ruoyi.system.mapper.house;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
com.ruoyi.system.domain.house.BusinessEntitySell
;
import
com.ruoyi.system.domain.house.BusinessEntitySell
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Param
;
...
@@ -11,7 +13,14 @@ import java.util.List;
...
@@ -11,7 +13,14 @@ import java.util.List;
public
interface
BusinessEntitySellMapper
{
public
interface
BusinessEntitySellMapper
{
// 插入业务实体销售信息;以经营主体侧记录一条修改日志
// 插入业务实体销售信息;以经营主体侧记录一条修改日志
// 字段级:以经营主体为 subject,记录 BE_YEAR_SELL 的新增
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"businessEntityInfoId"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"businessEntityInfoId"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"businessEntityInfoId"
,
fields
=
{
FieldCode
.
BE_YEAR_SELL
}
)
void
insertBusinessEntitySell
(
BusinessEntitySell
businessEntitySell
);
void
insertBusinessEntitySell
(
BusinessEntitySell
businessEntitySell
);
// 根据ID查询业务实体销售信息
// 根据ID查询业务实体销售信息
...
@@ -25,6 +34,13 @@ public interface BusinessEntitySellMapper {
...
@@ -25,6 +34,13 @@ public interface BusinessEntitySellMapper {
preLoadMapper
=
"com.ruoyi.system.mapper.house.BusinessEntitySellMapper.selectBusinessEntitySellById"
,
preLoadMapper
=
"com.ruoyi.system.mapper.house.BusinessEntitySellMapper.selectBusinessEntitySellById"
,
idExpr
=
"@preload[*].businessEntityInfoId"
idExpr
=
"@preload[*].businessEntityInfoId"
)
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
BUSINESS_ENTITY_INFO
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"@preload[*].businessEntityInfoId"
,
preLoadMapper
=
"com.ruoyi.system.mapper.house.BusinessEntitySellMapper.selectBusinessEntitySellById"
,
fields
=
{
FieldCode
.
BE_YEAR_SELL
}
)
void
updateBusinessEntitySell
(
BusinessEntitySell
businessEntitySell
);
void
updateBusinessEntitySell
(
BusinessEntitySell
businessEntitySell
);
// 删除业务实体销售信息;改为逻辑删除后以经营主体侧记录一条修改日志
// 删除业务实体销售信息;改为逻辑删除后以经营主体侧记录一条修改日志
...
...
ruoyi-system/src/main/java/com/ruoyi/system/mapper/house/HouseResourceMapper.java
View file @
e6ce768b
...
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
...
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.annotation.DataChangeLog
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.OperationType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
com.ruoyi.system.domain.house.HouseResource
;
import
com.ruoyi.system.domain.house.HouseResource
;
import
com.ruoyi.system.domain.house.vo.*
;
import
com.ruoyi.system.domain.house.vo.*
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Param
;
...
@@ -13,17 +15,57 @@ import java.util.List;
...
@@ -13,17 +15,57 @@ import java.util.List;
public
interface
HouseResourceMapper
{
public
interface
HouseResourceMapper
{
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"id"
,
fields
=
{
FieldCode
.
HR_BUSINESS_STATUS
,
FieldCode
.
HR_AREA
,
FieldCode
.
HR_EQUITY_TEL
,
FieldCode
.
HR_UNIT_PRICE
,
FieldCode
.
HR_IMAGE
,
FieldCode
.
HR_LICENSE_IMAGE
}
)
void
insertHouseResource
(
HouseResource
houseResource
);
void
insertHouseResource
(
HouseResource
houseResource
);
HouseResource
selectHouseResourceById
(
String
id
);
HouseResource
selectHouseResourceById
(
String
id
);
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
UPDATE
,
idExpr
=
"id"
,
preLoadMapper
=
"com.ruoyi.system.mapper.house.HouseResourceMapper.selectHouseResourceById"
,
fields
=
{
FieldCode
.
HR_BUSINESS_STATUS
,
FieldCode
.
HR_AREA
,
FieldCode
.
HR_EQUITY_TEL
,
FieldCode
.
HR_UNIT_PRICE
,
FieldCode
.
HR_IMAGE
,
FieldCode
.
HR_LICENSE_IMAGE
}
)
void
updateHouseResource
(
HouseResource
houseResource
);
void
updateHouseResource
(
HouseResource
houseResource
);
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
DELETE
,
idExpr
=
"."
)
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
DELETE
,
idExpr
=
"."
)
void
deleteHouseResourceById
(
String
id
);
void
deleteHouseResourceById
(
String
id
);
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"list[*].id"
)
@DataChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"list[*].id"
)
@DataFieldChangeLog
(
subjectType
=
SubjectType
.
HOUSE_RESOURCE
,
operation
=
OperationType
.
INSERT
,
idExpr
=
"list[*].id"
,
fields
=
{
FieldCode
.
HR_BUSINESS_STATUS
,
FieldCode
.
HR_AREA
,
FieldCode
.
HR_EQUITY_TEL
,
FieldCode
.
HR_UNIT_PRICE
,
FieldCode
.
HR_IMAGE
,
FieldCode
.
HR_LICENSE_IMAGE
}
)
void
batchInsertHouseResources
(
List
<
HouseResource
>
houseResources
);
void
batchInsertHouseResources
(
List
<
HouseResource
>
houseResources
);
IPage
<
HouseResourcePage
>
selectPage
(
IPage
<
HouseResource
>
page
,
@Param
(
"query"
)
HouseResourcePageQuery
houseResourcePageQuery
);
IPage
<
HouseResourcePage
>
selectPage
(
IPage
<
HouseResource
>
page
,
@Param
(
"query"
)
HouseResourcePageQuery
houseResourcePageQuery
);
...
...
ruoyi-system/src/main/java/com/ruoyi/system/service/fieldchangerecord/DataFieldChangeRecordService.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
service
.
fieldchangerecord
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordQuery
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordVO
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeSummaryRow
;
import
java.util.List
;
/**
* 字段级变更查询服务。
*
* <p>对外提供 HR / BE 两条独立的查询路径(明细 + 汇总)。写入由拦截器完成,
* 本服务不涉及写入。</p>
*/
public
interface
DataFieldChangeRecordService
{
IPage
<
FieldChangeRecordVO
>
pageHouseResourceFieldChanges
(
FieldChangeRecordQuery
query
);
List
<
FieldChangeSummaryRow
>
summaryHouseResourceFieldChanges
(
FieldChangeRecordQuery
query
);
IPage
<
FieldChangeRecordVO
>
pageBusinessEntityFieldChanges
(
FieldChangeRecordQuery
query
);
List
<
FieldChangeSummaryRow
>
summaryBusinessEntityFieldChanges
(
FieldChangeRecordQuery
query
);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/fieldchangerecord/impl/DataFieldChangeRecordServiceImpl.java
0 → 100644
View file @
e6ce768b
package
com
.
ruoyi
.
system
.
service
.
fieldchangerecord
.
impl
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.ruoyi.common.utils.SecurityUtils
;
import
com.ruoyi.system.domain.changerecord.enums.SubjectType
;
import
com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordQuery
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordVO
;
import
com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeSummaryRow
;
import
com.ruoyi.system.domain.grid.GridRegionUser
;
import
com.ruoyi.system.domain.grid.GridRegionUserExample
;
import
com.ruoyi.system.mapper.fieldchangerecord.DataFieldChangeRecordQueryMapper
;
import
com.ruoyi.system.mapper.grid.GridRegionUserMapper
;
import
com.ruoyi.system.service.fieldchangerecord.DataFieldChangeRecordService
;
import
com.ruoyi.system.service.house.impl.HouseResourceServiceImpl
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.CollectionUtils
;
import
java.util.Arrays
;
import
java.util.List
;
import
java.util.stream.Collectors
;
/**
* 字段级变更查询服务实现。
*
* <p>职责:</p>
* <ul>
* <li>参数校验(时间范围必填、start <= end);</li>
* <li>注入登录人网格权限 {@code wgCodes}(和 HR 侧 dataCollection 一致);</li>
* <li>当入参未指定 {@code fieldCodes} 时,补齐为当前主体侧的全部字段;</li>
* <li>调用 Mapper 查询;</li>
* <li>回填 {@code fieldLabel} / {@code valueType}(从 {@link FieldCode} 反查)。</li>
* </ul>
*
* <p>网格权限白名单与 HR 侧 {@code dataCollection} 完全一致。为避免循环依赖,
* 这里自己维护一份白名单 + 本地的 wgCodes 解析逻辑(和
* {@code HouseResourceServiceImpl.resolveCurrentUserWgCodes} 对齐)。</p>
*/
@Service
public
class
DataFieldChangeRecordServiceImpl
implements
DataFieldChangeRecordService
{
private
final
DataFieldChangeRecordQueryMapper
queryMapper
;
private
final
GridRegionUserMapper
gridRegionUserMapper
;
public
DataFieldChangeRecordServiceImpl
(
DataFieldChangeRecordQueryMapper
queryMapper
,
GridRegionUserMapper
gridRegionUserMapper
)
{
this
.
queryMapper
=
queryMapper
;
this
.
gridRegionUserMapper
=
gridRegionUserMapper
;
}
@Override
public
IPage
<
FieldChangeRecordVO
>
pageHouseResourceFieldChanges
(
FieldChangeRecordQuery
query
)
{
validateTimeRange
(
query
);
applyWgCodes
(
query
);
applyDefaultFieldCodes
(
query
,
SubjectType
.
HOUSE_RESOURCE
);
Page
<
FieldChangeRecordVO
>
page
=
new
Page
<>(
query
.
getPageNum
()
==
null
?
1
:
query
.
getPageNum
(),
query
.
getPageSize
()
==
null
?
20
:
query
.
getPageSize
());
IPage
<
FieldChangeRecordVO
>
result
=
queryMapper
.
selectHouseResourceFieldChanges
(
page
,
query
);
enrichRecords
(
result
.
getRecords
());
return
result
;
}
@Override
public
List
<
FieldChangeSummaryRow
>
summaryHouseResourceFieldChanges
(
FieldChangeRecordQuery
query
)
{
validateTimeRange
(
query
);
applyWgCodes
(
query
);
applyDefaultFieldCodes
(
query
,
SubjectType
.
HOUSE_RESOURCE
);
List
<
FieldChangeSummaryRow
>
rows
=
queryMapper
.
selectHouseResourceFieldChangeSummary
(
query
);
enrichSummary
(
rows
);
return
rows
;
}
@Override
public
IPage
<
FieldChangeRecordVO
>
pageBusinessEntityFieldChanges
(
FieldChangeRecordQuery
query
)
{
validateTimeRange
(
query
);
applyWgCodes
(
query
);
applyDefaultFieldCodes
(
query
,
SubjectType
.
BUSINESS_ENTITY_INFO
);
Page
<
FieldChangeRecordVO
>
page
=
new
Page
<>(
query
.
getPageNum
()
==
null
?
1
:
query
.
getPageNum
(),
query
.
getPageSize
()
==
null
?
20
:
query
.
getPageSize
());
IPage
<
FieldChangeRecordVO
>
result
=
queryMapper
.
selectBusinessEntityFieldChanges
(
page
,
query
);
enrichRecords
(
result
.
getRecords
());
return
result
;
}
@Override
public
List
<
FieldChangeSummaryRow
>
summaryBusinessEntityFieldChanges
(
FieldChangeRecordQuery
query
)
{
validateTimeRange
(
query
);
applyWgCodes
(
query
);
applyDefaultFieldCodes
(
query
,
SubjectType
.
BUSINESS_ENTITY_INFO
);
List
<
FieldChangeSummaryRow
>
rows
=
queryMapper
.
selectBusinessEntityFieldChangeSummary
(
query
);
enrichSummary
(
rows
);
return
rows
;
}
// -------------------------------------------------------------------------
private
void
validateTimeRange
(
FieldChangeRecordQuery
query
)
{
if
(
query
.
getChangeStartTime
()
==
null
||
query
.
getChangeEndTime
()
==
null
)
{
throw
new
IllegalArgumentException
(
"changeStartTime / changeEndTime 必填"
);
}
if
(
query
.
getChangeStartTime
().
after
(
query
.
getChangeEndTime
()))
{
throw
new
IllegalArgumentException
(
"changeStartTime 不能晚于 changeEndTime"
);
}
}
private
void
applyWgCodes
(
FieldChangeRecordQuery
query
)
{
List
<
String
>
wgCodes
=
resolveCurrentUserWgCodes
();
if
(
wgCodes
!=
null
)
{
query
.
setWgCodes
(
wgCodes
);
}
}
/**
* 与 {@code HouseResourceServiceImpl.resolveCurrentUserWgCodes(true)} 行为一致。
*/
private
List
<
String
>
resolveCurrentUserWgCodes
()
{
String
userId
=
SecurityUtils
.
getLoginUser
().
getUser
().
getUserId
();
GridRegionUserExample
example
=
new
GridRegionUserExample
();
example
.
createCriteria
().
andIsValidEqualTo
(
"1"
).
andUserIdEqualTo
(
userId
);
List
<
GridRegionUser
>
gridRegionUsers
=
gridRegionUserMapper
.
selectByExample
(
example
);
if
(
CollectionUtils
.
isEmpty
(
gridRegionUsers
)
||
SecurityUtils
.
getLoginUser
().
getUser
().
isAdmin
())
{
return
null
;
}
if
(
HouseResourceServiceImpl
.
WG_PERMISSION_BYPASS_USER_IDS
.
contains
(
userId
))
{
return
null
;
}
return
gridRegionUsers
.
stream
().
map
(
GridRegionUser:
:
getWgId
).
collect
(
Collectors
.
toList
());
}
/**
* 若入参没指定 fieldCodes,则按主体侧默认补齐全部字段。
* 若指定了 fieldCodes,按入参原样使用(后端不做属于哪个主体的校验——前端分 tab 自然不会传错)。
*/
private
void
applyDefaultFieldCodes
(
FieldChangeRecordQuery
query
,
SubjectType
subjectType
)
{
if
(
query
.
getFieldCodes
()
==
null
||
query
.
getFieldCodes
().
isEmpty
())
{
List
<
String
>
defaults
=
Arrays
.
stream
(
FieldCode
.
values
())
.
filter
(
f
->
f
.
getSubjectType
()
==
subjectType
)
.
map
(
Enum:
:
name
)
.
collect
(
Collectors
.
toList
());
query
.
setFieldCodes
(
defaults
);
}
}
private
void
enrichRecords
(
List
<
FieldChangeRecordVO
>
list
)
{
if
(
list
==
null
)
{
return
;
}
for
(
FieldChangeRecordVO
vo
:
list
)
{
FieldCode
fc
=
FieldCode
.
fromCode
(
vo
.
getFieldCode
());
if
(
fc
!=
null
)
{
vo
.
setFieldLabel
(
fc
.
getLabel
());
vo
.
setValueType
(
fc
.
getValueType
().
name
());
}
}
}
private
void
enrichSummary
(
List
<
FieldChangeSummaryRow
>
list
)
{
if
(
list
==
null
)
{
return
;
}
for
(
FieldChangeSummaryRow
row
:
list
)
{
FieldCode
fc
=
FieldCode
.
fromCode
(
row
.
getFieldCode
());
if
(
fc
!=
null
)
{
row
.
setFieldLabel
(
fc
.
getLabel
());
row
.
setValueType
(
fc
.
getValueType
().
name
());
}
}
}
}
ruoyi-system/src/main/java/com/ruoyi/system/service/house/impl/HouseResourceServiceImpl.java
View file @
e6ce768b
...
@@ -707,7 +707,7 @@ public class HouseResourceServiceImpl implements HouseResourceService {
...
@@ -707,7 +707,7 @@ public class HouseResourceServiceImpl implements HouseResourceService {
* 目前只给 {@link #pageHouseResources}、{@link #businessEntityStatistics}、{@link #houseResourceDataCollection}
* 目前只给 {@link #pageHouseResources}、{@link #businessEntityStatistics}、{@link #houseResourceDataCollection}
* 这类"列表/汇总"场景启用;{@link #exportListHouseResources} 保持原有行为不放开。
* 这类"列表/汇总"场景启用;{@link #exportListHouseResources} 保持原有行为不放开。
*/
*/
p
rivate
static
final
Set
<
String
>
WG_PERMISSION_BYPASS_USER_IDS
=
new
HashSet
<>(
Arrays
.
asList
(
p
ublic
static
final
Set
<
String
>
WG_PERMISSION_BYPASS_USER_IDS
=
new
HashSet
<>(
Arrays
.
asList
(
"794aa2c8b5c24933a30591dd7dc439ed"
,
"794aa2c8b5c24933a30591dd7dc439ed"
,
"ca1df7d1a3f347dc9e73e8283dd134a5"
,
"ca1df7d1a3f347dc9e73e8283dd134a5"
,
"3cf3b0fa6c0945919504be4aa237f89e"
));
"3cf3b0fa6c0945919504be4aa237f89e"
));
...
...
ruoyi-system/src/main/resources/mapper/fieldchangerecord/DataFieldChangeRecordMapper.xml
0 → 100644
View file @
e6ce768b
<?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.ruoyi.system.mapper.fieldchangerecord.DataFieldChangeRecordMapper"
>
<insert
id=
"batchInsert"
parameterType=
"java.util.List"
>
INSERT INTO data_field_change_record
(id, subject_type, subject_id, field_code, old_value, new_value, operation_type, operation_time)
VALUES
<foreach
collection=
"list"
item=
"item"
separator=
","
>
(#{item.id}, #{item.subjectType}, #{item.subjectId}, #{item.fieldCode},
#{item.oldValue}, #{item.newValue}, #{item.operationType}, #{item.operationTime})
</foreach>
</insert>
</mapper>
ruoyi-system/src/main/resources/mapper/fieldchangerecord/DataFieldChangeRecordQueryMapper.xml
0 → 100644
View file @
e6ce768b
<?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.ruoyi.system.mapper.fieldchangerecord.DataFieldChangeRecordQueryMapper"
>
<resultMap
id=
"FieldChangeRecordVO"
type=
"com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeRecordVO"
>
<id
property=
"id"
column=
"id"
/>
<result
property=
"subjectType"
column=
"subject_type"
/>
<result
property=
"subjectId"
column=
"subject_id"
/>
<result
property=
"subjectName"
column=
"subject_name"
/>
<result
property=
"fieldCode"
column=
"field_code"
/>
<result
property=
"oldValue"
column=
"old_value"
/>
<result
property=
"newValue"
column=
"new_value"
/>
<result
property=
"operationType"
column=
"operation_type"
/>
<result
property=
"operationTime"
column=
"operation_time"
/>
<result
property=
"hrType"
column=
"hr_type"
/>
<result
property=
"two"
column=
"two"
/>
<result
property=
"three"
column=
"three"
/>
<result
property=
"four"
column=
"four"
/>
<result
property=
"associatedHouseResourceId"
column=
"associated_hr_id"
/>
<result
property=
"associatedHouseResourceName"
column=
"associated_hr_name"
/>
</resultMap>
<resultMap
id=
"FieldChangeSummaryRow"
type=
"com.ruoyi.system.domain.fieldchangerecord.vo.FieldChangeSummaryRow"
>
<result
property=
"fieldCode"
column=
"field_code"
/>
<result
property=
"operationType"
column=
"operation_type"
/>
<result
property=
"count"
column=
"cnt"
/>
</resultMap>
<!-- =====================================================================
HR 侧
===================================================================== -->
<!-- 明细(分页由 MyBatis-Plus PaginationInterceptor 自动补 LIMIT / COUNT)。 -->
<select
id=
"selectHouseResourceFieldChanges"
resultMap=
"FieldChangeRecordVO"
>
SELECT
r.id,
r.subject_type,
r.subject_id,
CONCAT(IFNULL(hr.address, ''), IFNULL(hr.house_number, '')) AS subject_name,
r.field_code,
r.old_value,
r.new_value,
r.operation_type,
r.operation_time,
hr.type AS hr_type,
hr.two,
hr.three,
hr.four,
hr.id AS associated_hr_id,
CONCAT(IFNULL(hr.address, ''), IFNULL(hr.house_number, '')) AS associated_hr_name
FROM data_field_change_record r
INNER JOIN house_resource hr ON hr.id = r.subject_id AND hr.delete_flag = 0
<include
refid=
"hrSideWhere"
/>
ORDER BY r.operation_time DESC, r.id DESC
</select>
<!-- 汇总 -->
<select
id=
"selectHouseResourceFieldChangeSummary"
resultMap=
"FieldChangeSummaryRow"
>
SELECT r.field_code, r.operation_type, COUNT(*) AS cnt
FROM data_field_change_record r
INNER JOIN house_resource hr ON hr.id = r.subject_id AND hr.delete_flag = 0
<include
refid=
"hrSideWhere"
/>
GROUP BY r.field_code, r.operation_type
</select>
<sql
id=
"hrSideWhere"
>
<where>
r.subject_type = 'HOUSE_RESOURCE'
<if
test=
"query.changeStartTime != null"
>
AND r.operation_time
>
= #{query.changeStartTime}
</if>
<if
test=
"query.changeEndTime != null"
>
AND r.operation_time
<
= #{query.changeEndTime}
</if>
<if
test=
"query.fieldCodes != null and query.fieldCodes.size() > 0"
>
AND r.field_code IN
<foreach
collection=
"query.fieldCodes"
item=
"fc"
open=
"("
separator=
","
close=
")"
>
#{fc}
</foreach>
</if>
<if
test=
"query.operationTypes != null and query.operationTypes.size() > 0"
>
AND r.operation_type IN
<foreach
collection=
"query.operationTypes"
item=
"ot"
open=
"("
separator=
","
close=
")"
>
#{ot}
</foreach>
</if>
<if
test=
"query.hrTypes != null and query.hrTypes.size() > 0"
>
AND hr.type IN
<foreach
collection=
"query.hrTypes"
item=
"t"
open=
"("
separator=
","
close=
")"
>
#{t}
</foreach>
</if>
<if
test=
"query.two != null and query.two != ''"
>
AND hr.two = #{query.two}
</if>
<if
test=
"query.three != null and query.three != ''"
>
AND hr.three = #{query.three}
</if>
<if
test=
"query.four != null and query.four != ''"
>
AND hr.four = #{query.four}
</if>
<if
test=
"query.wgCodes != null and query.wgCodes.size() > 0"
>
AND hr.two IN
<foreach
collection=
"query.wgCodes"
item=
"wg"
open=
"("
separator=
","
close=
")"
>
#{wg}
</foreach>
</if>
</where>
</sql>
<!-- =====================================================================
BE 侧
===================================================================== -->
<!--
明细:data_field_change_record -> bei -> mapping -> hr2
一条 BE 日志对应 N 个关联房源时会展成 N 行;
前端带 hrTypes/网格筛选时天然按命中收敛,不带时交给外层 DISTINCT 收敛。
-->
<select
id=
"selectBusinessEntityFieldChanges"
resultMap=
"FieldChangeRecordVO"
>
SELECT
r.id,
r.subject_type,
r.subject_id,
bei.name AS subject_name,
r.field_code,
r.old_value,
r.new_value,
r.operation_type,
r.operation_time,
hr2.type AS hr_type,
hr2.two,
hr2.three,
hr2.four,
hr2.id AS associated_hr_id,
CONCAT(IFNULL(hr2.address, ''), IFNULL(hr2.house_number, '')) AS associated_hr_name
FROM data_field_change_record r
INNER JOIN business_entity_info bei
ON bei.id = r.subject_id AND bei.delete_flag = 0
INNER JOIN house_resource_business_entity_info_mapping m
ON m.business_entity_info_id = bei.id AND m.delete_flag = 0
INNER JOIN house_resource hr2
ON hr2.id = m.house_resource_id AND hr2.delete_flag = 0
<include
refid=
"beSideWhere"
/>
ORDER BY r.operation_time DESC, r.id DESC, hr2.id ASC
</select>
<!--
汇总:避免 JOIN 膨胀,用 COUNT(DISTINCT r.id) 按日志去重。
语义:某字段在时间窗内被改了多少次(不区分影响了多少关联房源)。
-->
<select
id=
"selectBusinessEntityFieldChangeSummary"
resultMap=
"FieldChangeSummaryRow"
>
SELECT r.field_code, r.operation_type, COUNT(DISTINCT r.id) AS cnt
FROM data_field_change_record r
INNER JOIN business_entity_info bei
ON bei.id = r.subject_id AND bei.delete_flag = 0
INNER JOIN house_resource_business_entity_info_mapping m
ON m.business_entity_info_id = bei.id AND m.delete_flag = 0
INNER JOIN house_resource hr2
ON hr2.id = m.house_resource_id AND hr2.delete_flag = 0
<include
refid=
"beSideWhere"
/>
GROUP BY r.field_code, r.operation_type
</select>
<sql
id=
"beSideWhere"
>
<where>
r.subject_type = 'BUSINESS_ENTITY_INFO'
<if
test=
"query.changeStartTime != null"
>
AND r.operation_time
>
= #{query.changeStartTime}
</if>
<if
test=
"query.changeEndTime != null"
>
AND r.operation_time
<
= #{query.changeEndTime}
</if>
<if
test=
"query.fieldCodes != null and query.fieldCodes.size() > 0"
>
AND r.field_code IN
<foreach
collection=
"query.fieldCodes"
item=
"fc"
open=
"("
separator=
","
close=
")"
>
#{fc}
</foreach>
</if>
<if
test=
"query.operationTypes != null and query.operationTypes.size() > 0"
>
AND r.operation_type IN
<foreach
collection=
"query.operationTypes"
item=
"ot"
open=
"("
separator=
","
close=
")"
>
#{ot}
</foreach>
</if>
<if
test=
"query.hrTypes != null and query.hrTypes.size() > 0"
>
AND hr2.type IN
<foreach
collection=
"query.hrTypes"
item=
"t"
open=
"("
separator=
","
close=
")"
>
#{t}
</foreach>
</if>
<if
test=
"query.two != null and query.two != ''"
>
AND hr2.two = #{query.two}
</if>
<if
test=
"query.three != null and query.three != ''"
>
AND hr2.three = #{query.three}
</if>
<if
test=
"query.four != null and query.four != ''"
>
AND hr2.four = #{query.four}
</if>
<if
test=
"query.wgCodes != null and query.wgCodes.size() > 0"
>
AND hr2.two IN
<foreach
collection=
"query.wgCodes"
item=
"wg"
open=
"("
separator=
","
close=
")"
>
#{wg}
</foreach>
</if>
</where>
</sql>
</mapper>
sql/data_field_change_record.sql
0 → 100644
View file @
e6ce768b
-- =================================================================
-- 字段级变更记录表
--
-- 用于记录对房源 (house_resource) 和经营主体 (business_entity_info /
-- business_entity_sell) 的指定字段进行 新增 / 修改 的 diff 日志。
-- 仅覆盖 operation_type IN (1, 2);逻辑删除继续走 data_change_record。
--
-- field_code / subject_type 取值见:
-- com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode
-- com.ruoyi.system.domain.changerecord.enums.SubjectType
-- =================================================================
CREATE
TABLE
IF
NOT
EXISTS
`data_field_change_record`
(
`id`
VARCHAR
(
64
)
NOT
NULL
COMMENT
'记录 id'
,
`subject_type`
VARCHAR
(
64
)
NOT
NULL
COMMENT
'HOUSE_RESOURCE / BUSINESS_ENTITY_INFO'
,
`subject_id`
VARCHAR
(
64
)
NOT
NULL
COMMENT
'主体 id(BE_YEAR_SELL 存 business_entity_info_id)'
,
`field_code`
VARCHAR
(
64
)
NOT
NULL
COMMENT
'业务语义字段码,详见 FieldCode 枚举'
,
`old_value`
TEXT
DEFAULT
NULL
COMMENT
'insert 时为 NULL;图片类字段直接存真实 URL 串'
,
`new_value`
TEXT
DEFAULT
NULL
COMMENT
'BE_YEAR_SELL 格式为 year=2025;value=523.50'
,
`operation_type`
TINYINT
NOT
NULL
COMMENT
'1 新增 2 修改'
,
`operation_time`
DATETIME
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
COMMENT
'操作时间'
,
PRIMARY
KEY
(
`id`
),
KEY
`idx_query`
(
`subject_type`
,
`field_code`
,
`operation_time`
),
KEY
`idx_subject`
(
`subject_type`
,
`subject_id`
,
`operation_time`
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'字段级变更记录表'
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment