lysync 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ """内置数据处理函数。
2
+
3
+ 这些函数通过 :func:`lysync.funcs.registry.register_func` 注册,可在
4
+ ``process_list`` 中按 ``func`` 名称引用。
5
+
6
+ * 作用于单个字段值的函数(``scope=FIELD``)签名:``func(value, **kwargs) -> value``
7
+ * 作用于整批数据的函数(``scope=DATASET``)签名:``func(rows, **kwargs) -> rows``
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import statistics
13
+
14
+ from .registry import FuncType, ProcessScope, register_func
15
+
16
+ # ============================ 字段级处理函数 (FIELD) ============================
17
+
18
+
19
+ @register_func("strip", FuncType.PROCESS, scope=ProcessScope.FIELD)
20
+ def strip(value, **kwargs):
21
+ """去除字符串首尾空白;非字符串原样返回。"""
22
+ if isinstance(value, str):
23
+ return value.strip()
24
+ return value
25
+
26
+
27
+ @register_func("to_int", FuncType.PROCESS, scope=ProcessScope.FIELD)
28
+ def to_int(value, default=None, **kwargs):
29
+ """转为 int;失败返回 ``default``。"""
30
+ if value is None or value == "":
31
+ return default
32
+ try:
33
+ if isinstance(value, str):
34
+ value = value.strip().replace(",", "")
35
+ return int(float(value))
36
+ except (ValueError, TypeError):
37
+ return default
38
+
39
+
40
+ @register_func("to_float", FuncType.PROCESS, scope=ProcessScope.FIELD)
41
+ def to_float(value, default=None, **kwargs):
42
+ """转为 float;失败返回 ``default``。"""
43
+ if value is None or value == "":
44
+ return default
45
+ try:
46
+ if isinstance(value, str):
47
+ value = value.strip().replace(",", "")
48
+ return float(value)
49
+ except (ValueError, TypeError):
50
+ return default
51
+
52
+
53
+ @register_func("default_if_none", FuncType.PROCESS, scope=ProcessScope.FIELD)
54
+ def default_if_none(value, default=None, **kwargs):
55
+ """当值为 ``None`` / 空字符串时返回 ``default``。"""
56
+ if value is None:
57
+ return default
58
+ if isinstance(value, str) and value.strip() == "":
59
+ return default
60
+ return value
61
+
62
+
63
+ @register_func("upper", FuncType.PROCESS, scope=ProcessScope.FIELD)
64
+ def upper(value, **kwargs):
65
+ """字符串转大写。"""
66
+ return value.upper() if isinstance(value, str) else value
67
+
68
+
69
+ @register_func("lower", FuncType.PROCESS, scope=ProcessScope.FIELD)
70
+ def lower(value, **kwargs):
71
+ """字符串转小写。"""
72
+ return value.lower() if isinstance(value, str) else value
73
+
74
+
75
+ @register_func("replace", FuncType.PROCESS, scope=ProcessScope.FIELD)
76
+ def replace(value, old="", new="", **kwargs):
77
+ """字符串替换,``old`` -> ``new``。"""
78
+ if not isinstance(value, str):
79
+ return value
80
+ return value.replace(old, new)
81
+
82
+
83
+ @register_func("bool_from_str", FuncType.PROCESS, scope=ProcessScope.FIELD)
84
+ def bool_from_str(value, true_values=None, false_values=None, default=None, **kwargs):
85
+ """依据字符串内容转为布尔值。
86
+
87
+ ``true_values`` / ``false_values`` 均可为逗号分隔字符串或列表,默认
88
+ 真值:``是,yes,y,true,1``;假值:``否,no,n,false,0``。
89
+ """
90
+ if value is None:
91
+ return default
92
+ if isinstance(value, bool):
93
+ return value
94
+
95
+ def _norm(x):
96
+ return (x or "").strip().lower()
97
+
98
+ if isinstance(true_values, str):
99
+ true_values = [t.strip() for t in true_values.split(",")]
100
+ if isinstance(false_values, str):
101
+ false_values = [t.strip() for t in false_values.split(",")]
102
+ true_values = [t.lower() for t in (true_values or ["是", "yes", "y", "true", "1"])]
103
+ false_values = [f.lower() for f in (false_values or ["否", "no", "n", "false", "0"])]
104
+
105
+ v = _norm(value)
106
+ if v in true_values:
107
+ return True
108
+ if v in false_values:
109
+ return False
110
+ return default
111
+
112
+
113
+ # ============================ 数据集级处理函数 (DATASET) ============================
114
+
115
+
116
+ @register_func("filter_by_field", FuncType.PROCESS, scope=ProcessScope.DATASET)
117
+ def filter_by_field(rows, field=None, op="==", value=None, **kwargs):
118
+ """过滤整批数据,仅保留满足条件的行。
119
+
120
+ ``op`` 支持 ``==, !=, >, >=, <, <=, in, not_in, contains``。
121
+ """
122
+ if not field:
123
+ return rows
124
+ result = []
125
+ for row in rows:
126
+ v = row.get(field)
127
+ keep = False
128
+ if op == "==":
129
+ keep = v == value
130
+ elif op == "!=":
131
+ keep = v != value
132
+ elif op == ">":
133
+ keep = v is not None and v > value
134
+ elif op == ">=":
135
+ keep = v is not None and v >= value
136
+ elif op == "<":
137
+ keep = v is not None and v < value
138
+ elif op == "<=":
139
+ keep = v is not None and v <= value
140
+ elif op == "in":
141
+ keep = v in (value or [])
142
+ elif op == "not_in":
143
+ keep = v not in (value or [])
144
+ elif op == "contains":
145
+ keep = value is not None and v is not None and str(value) in str(v)
146
+ if keep:
147
+ result.append(row)
148
+ return result
149
+
150
+
151
+ @register_func("deduplicate", FuncType.PROCESS, scope=ProcessScope.DATASET)
152
+ def deduplicate(rows, keys=None, keep="first", **kwargs):
153
+ """按 ``keys``(字段名列表,缺省使用全部字段)去重。
154
+
155
+ ``keep`` 为 ``first`` / ``last``。
156
+ """
157
+ if not rows:
158
+ return rows
159
+ if isinstance(keys, str):
160
+ keys = [k.strip() for k in keys.split(",")]
161
+ seen = {}
162
+ out = []
163
+ for idx, row in enumerate(rows):
164
+ if keys:
165
+ key = tuple(row.get(k) for k in keys)
166
+ else:
167
+ key = tuple(sorted(row.items()))
168
+ if key in seen:
169
+ if keep == "last":
170
+ # 用当前行替换之前保留的行
171
+ out[seen[key]] = row
172
+ continue
173
+ seen[key] = len(out)
174
+ out.append(row)
175
+ return out
176
+
177
+
178
+ @register_func("sort_rows", FuncType.PROCESS, scope=ProcessScope.DATASET)
179
+ def sort_rows(rows, field=None, desc=False, **kwargs):
180
+ """按 ``field`` 对整批数据排序(原地返回新列表)。"""
181
+ if not field or not rows:
182
+ return rows
183
+
184
+ def _key(r):
185
+ v = r.get(field)
186
+ return (v is None, v)
187
+
188
+ return sorted(rows, key=_key, reverse=bool(desc))
189
+
190
+
191
+ @register_func("query_counter_diff_robust", FuncType.PROCESS, scope=ProcessScope.DATASET)
192
+ def query_counter_diff_robust(
193
+ rows,
194
+ counter_field="value",
195
+ time_field="time",
196
+ lookback="auto",
197
+ recover_ratio=0.8,
198
+ recover_window=30,
199
+ min_valid_ratio=0.5,
200
+ spike_recover_abs=100,
201
+ reset_abs_threshold=100,
202
+ spike_abs_threshold=100,
203
+ diff_field="diff",
204
+ **kwargs,
205
+ ):
206
+ """对计数器型指标做“增量(diff)”计算,并对计数器复位 / 尖峰做鲁棒处理。
207
+
208
+ 适用场景:水/电/气等只增不减的累计读数,需要转换为每段增量。
209
+
210
+ Args:
211
+ counter_field: 计数字段名。
212
+ time_field: 时间字段名(用于排序,支持 datetime / 数值 / 可解析字符串)。
213
+ reset_abs_threshold: 当 ``当前值 - 上一条值 <= -该阈值`` 时判定为计数器复位,diff 归零。
214
+ spike_abs_threshold: 当增量绝对值超过该阈值时判定为疑似尖峰。
215
+ spike_recover_abs: 用于恢复尖峰的“正常增量”绝对值上限,超过则无法恢复、保留原值。
216
+ recover_window: 恢复尖峰时参考的近邻正常增量窗口大小(条数)。
217
+ lookback / recover_ratio / min_valid_ratio: 预留参数(保持与配置示例兼容,
218
+ 当前版本在样本不足时退化为保留原增量)。
219
+
220
+ Returns:
221
+ 在原 ``rows`` 的每一行追加 ``diff_field``(默认 ``diff``)字段后返回。
222
+ """
223
+ if not rows:
224
+ return rows
225
+
226
+ def _ts(r):
227
+ v = r.get(time_field)
228
+ if v is None:
229
+ return 0
230
+ if hasattr(v, "timestamp"):
231
+ return v.timestamp()
232
+ try:
233
+ return float(v)
234
+ except (TypeError, ValueError):
235
+ return 0
236
+
237
+ sorted_rows = sorted(rows, key=_ts)
238
+
239
+ valid_diffs = []
240
+ prev_value = None
241
+ for r in sorted_rows:
242
+ value = r.get(counter_field)
243
+ try:
244
+ value = float(value) if value is not None else None
245
+ except (TypeError, ValueError):
246
+ value = None
247
+
248
+ if value is None:
249
+ r[diff_field] = None
250
+ continue
251
+
252
+ if prev_value is None:
253
+ # 第一条有效样本,无法计算增量
254
+ r[diff_field] = 0.0
255
+ prev_value = value
256
+ continue
257
+
258
+ raw_diff = value - prev_value
259
+
260
+ # 计数器复位:累计值大幅下降
261
+ if raw_diff <= -reset_abs_threshold:
262
+ r[diff_field] = 0.0
263
+ prev_value = value
264
+ continue
265
+
266
+ # 疑似尖峰:增量异常偏大,尝试用近邻正常增量恢复
267
+ if raw_diff >= spike_abs_threshold:
268
+ if valid_diffs:
269
+ window = valid_diffs[-recover_window:] if recover_window else valid_diffs
270
+ median_diff = statistics.median(window)
271
+ if abs(median_diff) <= spike_recover_abs:
272
+ r[diff_field] = median_diff
273
+ valid_diffs.append(median_diff)
274
+ prev_value = value
275
+ continue
276
+ # 无法恢复,保留原始增量但仍计入统计
277
+ r[diff_field] = raw_diff
278
+ valid_diffs.append(raw_diff)
279
+ prev_value = value
280
+ continue
281
+
282
+ r[diff_field] = raw_diff
283
+ valid_diffs.append(raw_diff)
284
+ prev_value = value
285
+
286
+ return sorted_rows
@@ -0,0 +1,116 @@
1
+ """自定义函数注册中心。
2
+
3
+ 通过 :func:`register_func` 装饰器把处理函数登记到注册表中,供两处引用:
4
+
5
+ 1. 数据处理链 ``process_list`` —— 在 :class:`~lysync.models.SyncModelField`、
6
+ :class:`~lysync.models.SyncModelType`、:class:`~lysync.models.SyncExportTask`
7
+ 上配置,作为数据加工步骤。
8
+ 2. 采集自定义函数 ``custom_func`` —— 在 :class:`~lysync.models.SyncDataSource`
9
+ 上配置,作为 ``source_type='custom'`` 时的数据源采集逻辑。
10
+
11
+ 装饰器需要两个必填参数:
12
+
13
+ * ``config_name`` : 配置名称,即在 ``process_list`` / ``custom_func`` 字段中
14
+ 填写的名字。同一个名字可以同时存在于 “数据处理” 与 “采集” 两个命名空间。
15
+ * ``func_type`` : 函数类型,取值 ``FuncType.PROCESS`` 或 ``FuncType.COLLECT``。
16
+
17
+ 对于 ``func_type='process'`` 的函数,还可用 ``scope`` 声明作用范围:
18
+
19
+ * ``ProcessScope.FIELD`` : 作用于**单个字段的值**,签名 ``func(value, **kwargs) -> value``。
20
+ * ``ProcessScope.DATASET`` : 作用于**整批数据**,签名 ``func(rows, **kwargs) -> rows``。
21
+
22
+ 采集函数(``func_type='collect'``)签名为 ``func(**kwargs) -> list[dict]``,
23
+ 返回的列表中每个字典的键应与目标模型的字段名(``SyncModelField.field_name``)一致。
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class FuncType:
34
+ PROCESS = "process"
35
+ COLLECT = "collect"
36
+
37
+
38
+ class ProcessScope:
39
+ FIELD = "field"
40
+ DATASET = "dataset"
41
+
42
+
43
+ # config_name -> {"func": callable, "scope": str}
44
+ PROCESS_FUNCS: dict = {}
45
+
46
+ # config_name -> callable
47
+ COLLECT_FUNCS: dict = {}
48
+
49
+
50
+ def register_func(config_name: str, func_type: str, scope: str = ProcessScope.DATASET, override: bool = False):
51
+ """注册自定义函数的装饰器。
52
+
53
+ Args:
54
+ config_name: 配置名称(在 ``process_list`` / ``custom_func`` 中引用的名字)。
55
+ func_type: 函数类型,``FuncType.PROCESS`` 或 ``FuncType.COLLECT``。
56
+ scope: 仅对 ``func_type='process'`` 有效,``ProcessScope.FIELD`` 或
57
+ ``ProcessScope.DATASET``,默认 ``DATASET``。
58
+ override: 是否覆盖同名已注册函数,默认 ``False``(重复注册仅告警)。
59
+ """
60
+ if func_type not in (FuncType.PROCESS, FuncType.COLLECT):
61
+ raise ValueError(f"func_type 仅支持 {FuncType.PROCESS}/{FuncType.COLLECT},实际为 {func_type!r}")
62
+ if func_type == FuncType.PROCESS and scope not in (ProcessScope.FIELD, ProcessScope.DATASET):
63
+ raise ValueError(f"process 函数 scope 仅支持 {ProcessScope.FIELD}/{ProcessScope.DATASET},实际为 {scope!r}")
64
+
65
+ def decorator(func):
66
+ if func_type == FuncType.PROCESS:
67
+ if config_name in PROCESS_FUNCS and not override:
68
+ logger.warning("重复注册 process 函数: %s(使用 override=True 可覆盖)", config_name)
69
+ return func
70
+ PROCESS_FUNCS[config_name] = {"func": func, "scope": scope}
71
+ else:
72
+ if config_name in COLLECT_FUNCS and not override:
73
+ logger.warning("重复注册 collect 函数: %s(使用 override=True 可覆盖)", config_name)
74
+ return func
75
+ COLLECT_FUNCS[config_name] = func
76
+
77
+ func._lysync_config_name = config_name
78
+ func._lysync_func_type = func_type
79
+ func._lysync_scope = scope if func_type == FuncType.PROCESS else None
80
+ return func
81
+
82
+ return decorator
83
+
84
+
85
+ def register_process(config_name: str, scope: str = ProcessScope.DATASET, override: bool = False):
86
+ """便捷装饰器:注册一个数据处理函数。"""
87
+ return register_func(config_name, FuncType.PROCESS, scope=scope, override=override)
88
+
89
+
90
+ def register_collect(config_name: str, override: bool = False):
91
+ """便捷装饰器:注册一个采集函数。"""
92
+ return register_func(config_name, FuncType.COLLECT, override=override)
93
+
94
+
95
+ def get_process_func(config_name: str):
96
+ """返回 ``(func, scope)``,找不到时抛出 ``KeyError``。"""
97
+ item = PROCESS_FUNCS.get(config_name)
98
+ if not item:
99
+ raise KeyError(f"未找到 process 函数: {config_name!r},请确认已通过 @register_func 注册")
100
+ return item["func"], item["scope"]
101
+
102
+
103
+ def get_collect_func(config_name: str):
104
+ """返回采集函数,找不到时抛出 ``KeyError``。"""
105
+ func = COLLECT_FUNCS.get(config_name)
106
+ if not func:
107
+ raise KeyError(f"未找到 collect 函数: {config_name!r},请确认已通过 @register_func 注册")
108
+ return func
109
+
110
+
111
+ def list_registered():
112
+ """返回当前已注册函数的概览(用于调试/接口展示)。"""
113
+ return {
114
+ "process": {name: meta["scope"] for name, meta in PROCESS_FUNCS.items()},
115
+ "collect": list(COLLECT_FUNCS.keys()),
116
+ }
@@ -0,0 +1,207 @@
1
+ # Generated by Django 3.2.25 on 2026-07-11 11:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+ import django.utils.timezone
6
+ import util.base_model_fields
7
+ import util.basemodel
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ initial = True
13
+
14
+ dependencies = [
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='SyncDataSource',
20
+ fields=[
21
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
23
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
24
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
25
+ ('org_id', util.base_model_fields.IntegerField(verbose_name='所属组织')),
26
+ ('name', util.base_model_fields.CharField(max_length=50, verbose_name='数据源名称')),
27
+ ('source_type', util.base_model_fields.CharField(choices=[('db', '数据库'), ('custom', '自定义')], max_length=50, verbose_name='数据源类型')),
28
+ ('db_type', util.base_model_fields.CharField(blank=True, choices=[('mysql', 'MySQL'), ('tsql', 'SQL Server')], max_length=50, null=True, verbose_name='数据库类型')),
29
+ ('db_host', util.base_model_fields.CharField(blank=True, max_length=50, null=True, verbose_name='数据库主机')),
30
+ ('db_port', util.base_model_fields.IntegerField(blank=True, null=True, verbose_name='数据库端口')),
31
+ ('db_user', util.base_model_fields.CharField(blank=True, max_length=50, null=True, verbose_name='数据库用户')),
32
+ ('db_password', util.base_model_fields.CharField(blank=True, max_length=50, null=True, verbose_name='数据库密码')),
33
+ ('db_name', util.base_model_fields.CharField(blank=True, max_length=50, null=True, verbose_name='数据库名称')),
34
+ ('db_options', util.base_model_fields.JSONField(default=dict, verbose_name='数据库选项')),
35
+ ('db_connect_status', util.base_model_fields.CharField(blank=True, choices=[('checking', '检查中'), ('success', '连接成功'), ('fail', '连接失败')], editable=False, max_length=50, null=True, verbose_name='数据库连接状态')),
36
+ ('db_connect_message', util.base_model_fields.TextField(blank=True, editable=False, null=True, verbose_name='数据库连接消息')),
37
+ ('custom_func', util.base_model_fields.CharField(blank=True, max_length=50, null=True, verbose_name='自定义采集函数名称')),
38
+ ('custom_func_kwargs', util.base_model_fields.JSONField(default=dict, verbose_name='自定义采集函数参数')),
39
+ ('task_concurrency_enabled', util.base_model_fields.BooleanField(default=False, help_text='相同数据源下,所有任务是否能够并发执行。开启后受【最大任务并发数】限制;关闭后任务串行执行,调用间隔必须达到【任务调用最小间隔】。', verbose_name='任务可并发')),
40
+ ('task_max_concurrency', util.base_model_fields.IntegerField(default=1, help_text='相同数据源下允许同时运行的最大任务数(仅在【任务可并发】开启时生效),至少为 1。', verbose_name='最大任务并发数')),
41
+ ('task_call_interval', util.base_model_fields.IntegerField(default=0, help_text='相同数据源下任务不可并发时,两次调用之间必须间隔的最小秒数(仅在【任务可并发】关闭时生效)。', verbose_name='任务调用最小间隔(秒)')),
42
+ ],
43
+ options={
44
+ 'verbose_name': '数据源',
45
+ },
46
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
47
+ ),
48
+ migrations.CreateModel(
49
+ name='SyncExportTask',
50
+ fields=[
51
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
53
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
54
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
55
+ ('org_id', util.base_model_fields.IntegerField(verbose_name='所属组织')),
56
+ ('name', util.base_model_fields.CharField(max_length=50, verbose_name='任务名称')),
57
+ ('sql', util.base_model_fields.TextField(blank=True, null=True, verbose_name='SQL')),
58
+ ('explain_status', util.base_model_fields.CharField(choices=[('default', '未检测'), ('checking', '检测中'), ('A1', '最佳(A1)'), ('A2', '优秀(A2)'), ('B1', '良好(B1)'), ('B2', '良好(B2)'), ('C1', '一般(C1)'), ('C2', '一般(C2)'), ('D1', '较差(D1)'), ('D2', '较差(D2)'), ('E1', '极差(E1)'), ('failed', '失败')], default='default', editable=False, max_length=50, verbose_name='SQL性能')),
59
+ ('explain_json', util.base_model_fields.JSONField(blank=True, null=True, verbose_name='SQL性能结果')),
60
+ ('last_export_time', util.base_model_fields.DateTimeField(blank=True, editable=False, null=True, verbose_name='最后导出时间')),
61
+ ('last_export_unique_hash', util.base_model_fields.CharField(blank=True, editable=False, max_length=32, null=True, verbose_name='最后导出唯一标识')),
62
+ ('last_status', util.base_model_fields.CharField(blank=True, choices=[('checking', '检查中'), ('running', '运行中'), ('success', '成功'), ('fail', '失败'), ('stopping', '停止中'), ('stopped', '已停止'), ('wait', '等待中')], editable=False, max_length=50, null=True, verbose_name='最后状态')),
63
+ ('last_message', util.base_model_fields.TextField(blank=True, editable=False, null=True, verbose_name='最后消息')),
64
+ ('custom_func_kwargs', util.base_model_fields.JSONField(help_text='此参数会合并数据源的自定义采集函数参数, 此任务参数的优先级较高', verbose_name='自定义采集函数参数')),
65
+ ('process_list', util.base_model_fields.JSONField(default=list, verbose_name='处理函数列表')),
66
+ ('self_concurrency_enabled', util.base_model_fields.BooleanField(default=False, help_text='相同任务是否能够并发执行。开启后受【自身最大并发数】限制;关闭后同一任务串行执行,调用间隔必须达到【自身调用最小间隔】。', verbose_name='自身可并发')),
67
+ ('self_max_concurrency', util.base_model_fields.IntegerField(default=1, help_text='相同任务允许同时运行的最大实例数(仅在【自身可并发】开启时生效),至少为 1。', verbose_name='自身最大并发数')),
68
+ ('self_call_interval', util.base_model_fields.IntegerField(default=0, help_text='相同任务不可并发时,两次调用之间必须间隔的最小秒数(仅在【自身可并发】关闭时生效)。', verbose_name='自身调用最小间隔(秒)')),
69
+ ('data_start_date', util.base_model_fields.DateField(blank=True, null=True, verbose_name='数据开始日期')),
70
+ ('data_source', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncdatasource', verbose_name='数据源')),
71
+ ],
72
+ options={
73
+ 'verbose_name': '数据导出任务',
74
+ },
75
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
76
+ ),
77
+ migrations.CreateModel(
78
+ name='SyncModelType',
79
+ fields=[
80
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
81
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
82
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
83
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
84
+ ('name', util.base_model_fields.CharField(db_index=True, max_length=50, verbose_name='模型名称')),
85
+ ('app_label', util.base_model_fields.CharField(db_index=True, max_length=100, verbose_name='应用标签')),
86
+ ('model', util.base_model_fields.CharField(db_index=True, max_length=100, verbose_name='模型名')),
87
+ ('sorted', util.base_model_fields.IntegerField(db_index=True, default=0, verbose_name='数据处理排序')),
88
+ ('process_list', util.base_model_fields.JSONField(default=list, verbose_name='处理函数列表')),
89
+ ],
90
+ options={
91
+ 'verbose_name': '数据模型',
92
+ 'ordering': ['sorted'],
93
+ },
94
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
95
+ ),
96
+ migrations.CreateModel(
97
+ name='SyncModelField',
98
+ fields=[
99
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
100
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
101
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
102
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
103
+ ('name', util.base_model_fields.CharField(db_index=True, max_length=50, verbose_name='字段名')),
104
+ ('field_name', util.base_model_fields.CharField(db_index=True, max_length=50, verbose_name='字段名')),
105
+ ('is_unique', util.base_model_fields.BooleanField(default=False, help_text='可以配置多个字段,用于唯一标识数据行', verbose_name='是否唯一字段')),
106
+ ('is_datetime_index', util.base_model_fields.BooleanField(default=False, help_text='只能配置一个字段,用户数据同步时确定时间范围', verbose_name='是否日期时间索引')),
107
+ ('process_list', util.base_model_fields.JSONField(default=list, verbose_name='处理函数列表')),
108
+ ('is_import_column', util.base_model_fields.BooleanField(choices=[(True, '导入'), (False, '非导入')], default=True, verbose_name='是否为导入列')),
109
+ ('model_type', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncmodeltype', verbose_name='数据模型')),
110
+ ],
111
+ options={
112
+ 'verbose_name': '数据字段',
113
+ },
114
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
115
+ ),
116
+ migrations.CreateModel(
117
+ name='SyncExportTaskHistory',
118
+ fields=[
119
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
120
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
121
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
122
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
123
+ ('sql', util.base_model_fields.TextField(verbose_name='SQL')),
124
+ ('export_task', util.base_model_fields.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lysync.syncexporttask', verbose_name='任务')),
125
+ ],
126
+ options={
127
+ 'verbose_name': '数据导出任务历史',
128
+ 'ordering': ['id'],
129
+ },
130
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
131
+ ),
132
+ migrations.AddField(
133
+ model_name='syncexporttask',
134
+ name='model_type',
135
+ field=util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncmodeltype', verbose_name='数据模型'),
136
+ ),
137
+ migrations.CreateModel(
138
+ name='SyncExportRecord',
139
+ fields=[
140
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
141
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
142
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
143
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
144
+ ('org_id', util.base_model_fields.IntegerField(verbose_name='所属组织')),
145
+ ('original_sql', util.base_model_fields.TextField(verbose_name='原始 SQL')),
146
+ ('exec_sql', util.base_model_fields.TextField(verbose_name='执行 SQL')),
147
+ ('export_status', util.base_model_fields.CharField(choices=[('checking', '检查中'), ('running', '运行中'), ('success', '成功'), ('fail', '失败'), ('stopping', '停止中'), ('stopped', '已停止'), ('wait', '等待中')], max_length=50, verbose_name='导出状态')),
148
+ ('check_status', util.base_model_fields.CharField(blank=True, choices=[('complete', '完成'), ('wait', '等待检查'), ('checking', '检查中')], max_length=50, null=True, verbose_name='检查状态')),
149
+ ('message', util.base_model_fields.TextField(verbose_name='导出消息')),
150
+ ('data_date', util.base_model_fields.DateField(blank=True, null=True, verbose_name='数据日期')),
151
+ ('export_unique_hash_list', util.base_model_fields.JSONField(blank=True, null=True, verbose_name='导出数据唯一标识列表')),
152
+ ('export_count', util.base_model_fields.IntegerField(default=0, verbose_name='导出数据数量')),
153
+ ('export_total_count', util.base_model_fields.IntegerField(default=0, help_text='导出数据总数,包含补充数据量', verbose_name='导出数据总数')),
154
+ ('last_check_message', util.base_model_fields.TextField(blank=True, editable=False, null=True, verbose_name='最后补录消息')),
155
+ ('last_check_record_id', util.base_model_fields.IntegerField(blank=True, editable=False, null=True, verbose_name='最后补录记录ID')),
156
+ ('success_check_record_count', util.base_model_fields.IntegerField(default=0, verbose_name='成功补录数量')),
157
+ ('last_check_time', util.base_model_fields.DateTimeField(blank=True, editable=False, null=True, verbose_name='最后补录时间')),
158
+ ('export_task', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncexporttask', verbose_name='任务')),
159
+ ],
160
+ options={
161
+ 'verbose_name': '数据导出记录',
162
+ 'ordering': ['id'],
163
+ },
164
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
165
+ ),
166
+ migrations.CreateModel(
167
+ name='SyncExportCheckRecord',
168
+ fields=[
169
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
170
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
171
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
172
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
173
+ ('org_id', util.base_model_fields.IntegerField(verbose_name='所属组织')),
174
+ ('original_sql', util.base_model_fields.TextField(verbose_name='原始 SQL')),
175
+ ('exec_sql', util.base_model_fields.TextField(verbose_name='执行 SQL')),
176
+ ('export_status', util.base_model_fields.CharField(choices=[('checking', '检查中'), ('running', '运行中'), ('success', '成功'), ('fail', '失败'), ('stopping', '停止中'), ('stopped', '已停止'), ('wait', '等待中')], max_length=50, verbose_name='导出状态')),
177
+ ('message', util.base_model_fields.TextField(verbose_name='导出消息')),
178
+ ('export_unique_hash_list', util.base_model_fields.JSONField(blank=True, null=True, verbose_name='导出数据唯一标识列表')),
179
+ ('export_count', util.base_model_fields.IntegerField(default=0, verbose_name='导出数据数量')),
180
+ ('export_record', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncexportrecord', verbose_name='导出记录')),
181
+ ('export_task', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncexporttask', verbose_name='任务')),
182
+ ],
183
+ options={
184
+ 'verbose_name': '数据导出补充记录',
185
+ 'ordering': ['id'],
186
+ },
187
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
188
+ ),
189
+ migrations.CreateModel(
190
+ name='SyncDataFingerprint',
191
+ fields=[
192
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
193
+ ('is_active', util.base_model_fields.BooleanField(choices=[(True, '可用'), (False, '禁用')], default=True, verbose_name='是否可用')),
194
+ ('create_time', util.base_model_fields.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间')),
195
+ ('update_time', util.base_model_fields.DateTimeField(auto_now=True, db_index=True, verbose_name='更新时间')),
196
+ ('org_id', util.base_model_fields.IntegerField(verbose_name='所属组织')),
197
+ ('origin_recode_unique_hash', util.base_model_fields.CharField(db_index=True, max_length=32, verbose_name='原始数据唯一标识')),
198
+ ('fingerprint', util.base_model_fields.CharField(max_length=32, verbose_name='数据指纹')),
199
+ ('export_task', util.base_model_fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lysync.syncexporttask', verbose_name='任务')),
200
+ ],
201
+ options={
202
+ 'verbose_name': '数据指纹',
203
+ 'unique_together': {('export_task', 'origin_recode_unique_hash')},
204
+ },
205
+ bases=(models.Model, util.basemodel.JSONBaseMixin, util.basemodel.ModefyMixin),
206
+ ),
207
+ ]
@@ -0,0 +1,19 @@
1
+ # Generated by Django 3.2.25 on 2026-07-11 17:54
2
+
3
+ from django.db import migrations
4
+ import util.base_model_fields
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('lysync', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='syncexporttask',
16
+ name='explain_json',
17
+ field=util.base_model_fields.JSONField(blank=True, editable=False, null=True, verbose_name='SQL性能结果'),
18
+ ),
19
+ ]
File without changes