进行了 backend 的重构,实验中

This commit is contained in:
2026-06-18 04:15:23 +08:00
parent 54cc658f0b
commit 854c832c07
10 changed files with 1196 additions and 864 deletions

View File

@@ -1,6 +1,7 @@
# Generated by Django 4.2.27 on 2026-06-17 17:17 # Generated by Django 4.2.27 on 2026-06-18 03:51
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -11,173 +12,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='Permission',
fields=[
('perm_id', models.AutoField(primary_key=True, serialize=False, verbose_name='权限ID')),
('perm_code', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='权限编码')),
('perm_name', models.CharField(max_length=100, verbose_name='权限名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '权限',
'verbose_name_plural': '权限',
'db_table': 'permission',
'managed': False,
},
),
migrations.CreateModel(
name='Role',
fields=[
('role_id', models.AutoField(primary_key=True, serialize=False, verbose_name='角色ID')),
('role_code', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='角色编码')),
('role_name', models.CharField(max_length=50, verbose_name='角色名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '角色',
'verbose_name_plural': '角色',
'db_table': 'role',
'managed': False,
},
),
migrations.CreateModel(
name='RolePermission',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '角色权限关联',
'verbose_name_plural': '角色权限关联',
'db_table': 'role_permission',
'managed': False,
},
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '用户角色关联',
'verbose_name_plural': '用户角色关联',
'db_table': 'user_role',
'managed': False,
},
),
migrations.CreateModel(
name='ZuzhangRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='组长用户ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('yaoqing_guanshi_shu', models.IntegerField(default=0, verbose_name='邀请管事总数')),
('shouru_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')),
('fenyong_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')),
],
options={
'verbose_name': '组长每日统计',
'db_table': 'zuzhang_ri_tongji',
'indexes': [models.Index(fields=['riqi', '-yaoqing_guanshi_shu'], name='zuzhang_ri__riqi_87169e_idx'), models.Index(fields=['riqi', '-shouru_zonge'], name='zuzhang_ri__riqi_1d7e51_idx')],
'unique_together': {('yonghuid', 'riqi')},
},
),
migrations.CreateModel(
name='TixianRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], verbose_name='用户类型')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='当天提现总额(元)')),
],
options={
'verbose_name': '每日提现统计',
'db_table': 'tixian_ri_tongji',
'indexes': [models.Index(fields=['riqi', 'leixing'], name='tixian_ri_t_riqi_c18bb2_idx')],
'unique_together': {('riqi', 'leixing')},
},
),
migrations.CreateModel(
name='ShangjiaRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='商家用户ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('paifa_dingdan_shu', models.IntegerField(default=0, verbose_name='派发订单总数')),
('paifa_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派发订单总金额')),
('jiesuan_dingdan_shu', models.IntegerField(default=0, verbose_name='结算订单总数')),
('jiesuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='结算总金额')),
('tuikuan_dingdan_shu', models.IntegerField(default=0, verbose_name='退款订单总数')),
('tuikuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '商家每日统计',
'db_table': 'shangjia_ri_tongji',
'indexes': [models.Index(fields=['riqi', '-paifa_jine'], name='shangjia_ri_riqi_d00f7f_idx'), models.Index(fields=['riqi', '-jiesuan_jine'], name='shangjia_ri_riqi_25772a_idx'), models.Index(fields=['riqi', '-paifa_dingdan_shu'], name='shangjia_ri_riqi_e05c54_idx')],
'unique_together': {('yonghuid', 'riqi')},
},
),
migrations.CreateModel(
name='GuanshiXufeiRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='管事用户ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('xufei_zongshu', models.IntegerField(default=0, verbose_name='今日续费总数')),
('xufei_shouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')),
],
options={
'verbose_name': '管事续费每日统计',
'db_table': 'guanshi_xufei_ri_tongji',
'indexes': [models.Index(fields=['riqi', '-xufei_zongshu'], name='guanshi_xuf_riqi_7c128f_idx'), models.Index(fields=['riqi', '-xufei_shouyi'], name='guanshi_xuf_riqi_8f09b5_idx')],
'unique_together': {('yonghuid', 'riqi')},
},
),
migrations.CreateModel(
name='GuanshiRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='管事用户ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('yaoqing_dashou_shu', models.IntegerField(default=0, verbose_name='邀请打手总数')),
('chongzhi_dashou_shu', models.IntegerField(default=0, verbose_name='充值打手数量')),
('shouru_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额')),
],
options={
'verbose_name': '管事每日统计',
'db_table': 'guanshi_ri_tongji',
'indexes': [models.Index(fields=['riqi', '-yaoqing_dashou_shu'], name='guanshi_ri__riqi_075e1a_idx'), models.Index(fields=['riqi', '-chongzhi_dashou_shu'], name='guanshi_ri__riqi_78a79d_idx'), models.Index(fields=['riqi', '-shouru_zonge'], name='guanshi_ri__riqi_334f52_idx')],
'unique_together': {('yonghuid', 'riqi')},
},
),
migrations.CreateModel(
name='DashouRiTongji',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='打手用户ID')),
('riqi', models.DateField(db_index=True, verbose_name='日期')),
('jiedan_zongliang', models.IntegerField(default=0, verbose_name='接单总量')),
('chengjiao_zongliang', models.IntegerField(default=0, verbose_name='成交单总量')),
('jiedan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')),
('chengjiao_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')),
('tuikuan_liang', models.IntegerField(default=0, verbose_name='退款订单量')),
('tuikuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '打手每日统计',
'db_table': 'dashou_ri_tongji',
'indexes': [models.Index(fields=['riqi', '-jiedan_zongliang'], name='dashou_ri_t_riqi_95a785_idx'), models.Index(fields=['riqi', '-chengjiao_zongliang'], name='dashou_ri_t_riqi_ad9c1a_idx'), models.Index(fields=['riqi', '-jiedan_zonge'], name='dashou_ri_t_riqi_5da703_idx'), models.Index(fields=['riqi', '-chengjiao_zonge'], name='dashou_ri_t_riqi_0a3156_idx')],
'unique_together': {('yonghuid', 'riqi')},
},
),
migrations.CreateModel( migrations.CreateModel(
name='AbnormalUserLog', name='AbnormalUserLog',
fields=[ fields=[
@@ -200,7 +34,270 @@ class Migration(migrations.Migration):
'verbose_name': '异常用户日志', 'verbose_name': '异常用户日志',
'verbose_name_plural': '异常用户日志', 'verbose_name_plural': '异常用户日志',
'db_table': 'abnormal_user_log', 'db_table': 'abnormal_user_log',
'indexes': [models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'), models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'), models.Index(fields=['create_time'], name='abnormal_us_create__ee16da_idx')],
}, },
), ),
migrations.CreateModel(
name='LeaderDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('LeaderID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='组长用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('InvitedManagerCount', models.IntegerField(db_column='yaoqing_guanshi_shu', default=0, verbose_name='邀请管事总数')),
('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')),
('CommissionAmount', models.DecimalField(db_column='fenyong_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')),
],
options={
'verbose_name': '组长每日统计',
'db_table': 'zuzhang_ri_tongji',
},
),
migrations.CreateModel(
name='ManagerDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('InvitedPlayerCount', models.IntegerField(db_column='yaoqing_dashou_shu', default=0, verbose_name='邀请打手总数')),
('RechargedPlayerCount', models.IntegerField(db_column='chongzhi_dashou_shu', default=0, verbose_name='充值打手数量')),
('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额')),
],
options={
'verbose_name': '管事每日统计',
'db_table': 'guanshi_ri_tongji',
},
),
migrations.CreateModel(
name='ManagerRenewalDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('RenewalTotal', models.IntegerField(db_column='xufei_zongshu', default=0, verbose_name='今日续费总数')),
('RenewalRevenue', models.DecimalField(db_column='xufei_shouyi', decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')),
],
options={
'verbose_name': '管事续费每日统计',
'db_table': 'guanshi_xufei_ri_tongji',
},
),
migrations.CreateModel(
name='MerchantDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('MerchantID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='商家用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('AssignedOrderCount', models.IntegerField(db_column='paifa_dingdan_shu', default=0, verbose_name='派发订单总数')),
('AssignedAmount', models.DecimalField(db_column='paifa_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='派发订单总金额')),
('SettledOrderCount', models.IntegerField(db_column='jiesuan_dingdan_shu', default=0, verbose_name='结算订单总数')),
('SettledAmount', models.DecimalField(db_column='jiesuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='结算总金额')),
('RefundOrderCount', models.IntegerField(db_column='tuikuan_dingdan_shu', default=0, verbose_name='退款订单总数')),
('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '商家每日统计',
'db_table': 'shangjia_ri_tongji',
},
),
migrations.CreateModel(
name='Permission',
fields=[
('perm_id', models.AutoField(primary_key=True, serialize=False, verbose_name='权限ID')),
('perm_code', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='权限编码')),
('perm_name', models.CharField(max_length=100, verbose_name='权限名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '权限',
'verbose_name_plural': '权限',
'db_table': 'permission',
'managed': True,
},
),
migrations.CreateModel(
name='PlayerDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('PlayerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='打手用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('AcceptedOrderTotal', models.IntegerField(db_column='jiedan_zongliang', default=0, verbose_name='接单总量')),
('CompletedOrderTotal', models.IntegerField(db_column='chengjiao_zongliang', default=0, verbose_name='成交单总量')),
('AcceptedAmount', models.DecimalField(db_column='jiedan_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')),
('CompletedAmount', models.DecimalField(db_column='chengjiao_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')),
('RefundCount', models.IntegerField(db_column='tuikuan_liang', default=0, verbose_name='退款订单量')),
('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '打手每日统计',
'db_table': 'dashou_ri_tongji',
},
),
migrations.CreateModel(
name='Role',
fields=[
('role_id', models.AutoField(primary_key=True, serialize=False, verbose_name='角色ID')),
('role_code', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='角色编码')),
('role_name', models.CharField(max_length=50, verbose_name='角色名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '角色',
'verbose_name_plural': '角色',
'db_table': 'role',
'managed': True,
},
),
migrations.CreateModel(
name='WithdrawalDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('WithdrawalType', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], db_column='leixing', verbose_name='用户类型')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='当天提现总额(元)')),
],
options={
'verbose_name': '每日提现统计',
'db_table': 'tixian_ri_tongji',
'indexes': [models.Index(fields=['Date', 'WithdrawalType'], name='tixian_ri_t_riqi_c18bb2_idx')],
'unique_together': {('Date', 'WithdrawalType')},
},
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
],
options={
'verbose_name': '用户角色关联',
'verbose_name_plural': '用户角色关联',
'db_table': 'user_role',
'managed': True,
},
),
migrations.CreateModel(
name='RolePermission',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
],
options={
'verbose_name': '角色权限关联',
'verbose_name_plural': '角色权限关联',
'db_table': 'role_permission',
'managed': True,
},
),
migrations.AddIndex(
model_name='role',
index=models.Index(fields=['role_code'], name='role_role_co_b9f497_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-AcceptedOrderTotal'], name='dashou_ri_t_riqi_95a785_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-CompletedOrderTotal'], name='dashou_ri_t_riqi_ad9c1a_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-AcceptedAmount'], name='dashou_ri_t_riqi_5da703_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-CompletedAmount'], name='dashou_ri_t_riqi_0a3156_idx'),
),
migrations.AlterUniqueTogether(
name='playerdailystats',
unique_together={('PlayerID', 'Date')},
),
migrations.AddIndex(
model_name='permission',
index=models.Index(fields=['perm_code'], name='permission_perm_co_56cbe3_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-AssignedAmount'], name='shangjia_ri_riqi_d00f7f_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-SettledAmount'], name='shangjia_ri_riqi_25772a_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-AssignedOrderCount'], name='shangjia_ri_riqi_e05c54_idx'),
),
migrations.AlterUniqueTogether(
name='merchantdailystats',
unique_together={('MerchantID', 'Date')},
),
migrations.AddIndex(
model_name='managerrenewaldailystats',
index=models.Index(fields=['Date', '-RenewalTotal'], name='guanshi_xuf_riqi_7c128f_idx'),
),
migrations.AddIndex(
model_name='managerrenewaldailystats',
index=models.Index(fields=['Date', '-RenewalRevenue'], name='guanshi_xuf_riqi_8f09b5_idx'),
),
migrations.AlterUniqueTogether(
name='managerrenewaldailystats',
unique_together={('ManagerID', 'Date')},
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-InvitedPlayerCount'], name='guanshi_ri__riqi_075e1a_idx'),
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-RechargedPlayerCount'], name='guanshi_ri__riqi_78a79d_idx'),
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-TotalIncome'], name='guanshi_ri__riqi_334f52_idx'),
),
migrations.AlterUniqueTogether(
name='managerdailystats',
unique_together={('ManagerID', 'Date')},
),
migrations.AddIndex(
model_name='leaderdailystats',
index=models.Index(fields=['Date', '-InvitedManagerCount'], name='zuzhang_ri__riqi_87169e_idx'),
),
migrations.AddIndex(
model_name='leaderdailystats',
index=models.Index(fields=['Date', '-TotalIncome'], name='zuzhang_ri__riqi_1d7e51_idx'),
),
migrations.AlterUniqueTogether(
name='leaderdailystats',
unique_together={('LeaderID', 'Date')},
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'),
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'),
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['create_time'], name='abnormal_us_create__ee16da_idx'),
),
migrations.AlterUniqueTogether(
name='userrole',
unique_together={('account_id', 'role')},
),
migrations.AlterUniqueTogether(
name='rolepermission',
unique_together={('role', 'permission')},
),
] ]

View File

@@ -17,7 +17,7 @@ class Role(QModel):
db_table = 'role' db_table = 'role'
verbose_name = '角色' verbose_name = '角色'
verbose_name_plural = '角色' verbose_name_plural = '角色'
managed = False managed = True
indexes = [ indexes = [
models.Index(fields=['role_code']), models.Index(fields=['role_code']),
] ]
@@ -40,7 +40,7 @@ class Permission(QModel):
db_table = 'permission' db_table = 'permission'
verbose_name = '权限' verbose_name = '权限'
verbose_name_plural = '权限' verbose_name_plural = '权限'
managed = False managed = True
indexes = [ indexes = [
models.Index(fields=['perm_code']), models.Index(fields=['perm_code']),
] ]
@@ -61,8 +61,8 @@ class RolePermission(QModel):
db_table = 'role_permission' db_table = 'role_permission'
verbose_name = '角色权限关联' verbose_name = '角色权限关联'
verbose_name_plural = '角色权限关联' verbose_name_plural = '角色权限关联'
managed = False managed = True
unique_together = [['role', 'permission']] # 防止重复授权 unique_together = [['role', 'permission']]
def __str__(self): def __str__(self):
return f"{self.role.role_name} -> {self.permission.perm_name}" return f"{self.role.role_name} -> {self.permission.perm_name}"
@@ -72,7 +72,7 @@ class RolePermission(QModel):
class UserRole(QModel): class UserRole(QModel):
"""用户角色关联表""" """用户角色关联表"""
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段 account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID')
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
@@ -81,37 +81,28 @@ class UserRole(QModel):
db_table = 'user_role' db_table = 'user_role'
verbose_name = '用户角色关联' verbose_name = '用户角色关联'
verbose_name_plural = '用户角色关联' verbose_name_plural = '用户角色关联'
managed = False managed = True
unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条 unique_together = [['account_id', 'role']]
def __str__(self): def __str__(self):
return f"{self.account_id} -> {self.role.role_name}" return f"{self.account_id} -> {self.role.role_name}"
# ==================== 5. 异常用户日志 ====================
class AbnormalUserLog(QModel): class AbnormalUserLog(QModel):
"""记录恶意尝试越权操作的用户信息""" """记录恶意尝试越权操作的用户信息"""
# 账号信息
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号IDphone') account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号IDphone')
# 网络信息
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址') ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址')
real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP穿透代理后') real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP穿透代理后')
x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For') x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For')
user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent') user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent')
# 地理位置信息需借助第三方API这里只存储请求时的IP后续可离线解析
# 但我们可以存储更多请求细节
request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法') request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法')
request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径') request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径')
request_params = models.TextField(blank=True, default='', verbose_name='请求参数GET/POST') request_params = models.TextField(blank=True, default='', verbose_name='请求参数GET/POST')
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)') request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)')
request_headers = models.TextField(blank=True, default='', verbose_name='请求头JSON格式') request_headers = models.TextField(blank=True, default='', verbose_name='请求头JSON格式')
# 攻击特征
attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型') attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型')
detail = models.TextField(blank=True, default='', verbose_name='详细描述') detail = models.TextField(blank=True, default='', verbose_name='详细描述')
# 时间戳
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta: class Meta:
@@ -128,148 +119,206 @@ class AbnormalUserLog(QModel):
return f"{self.account_id} @ {self.ip_address} - {self.create_time}" return f"{self.account_id} @ {self.ip_address} - {self.create_time}"
# ==================== 6. 商家每日统计 ====================
class MerchantDailyStats(QModel):
"""商家每日统计表(原 ShangjiaRiTongji"""
class ShangjiaRiTongji(QModel): MerchantID = models.CharField(
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID') max_length=7, db_index=True, db_column='yonghuid', verbose_name='商家用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
# 派发统计 # 派发统计
paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数') AssignedOrderCount = models.IntegerField(
paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额') default=0, db_column='paifa_dingdan_shu', verbose_name='派发订单总')
AssignedAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='paifa_jine', verbose_name='派发订单总金额')
# 结算统计(成交) # 结算统计
jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数') SettledOrderCount = models.IntegerField(
jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额') default=0, db_column='jiesuan_dingdan_shu', verbose_name='结算订单总数')
SettledAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='jiesuan_jine', verbose_name='结算总金额')
# 退款统计(可选) # 退款统计
tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数') RefundOrderCount = models.IntegerField(
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') default=0, db_column='tuikuan_dingdan_shu', verbose_name='退款订单总数')
RefundAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='tuikuan_jine', verbose_name='退款总金额')
class Meta: class Meta:
db_table = 'shangjia_ri_tongji' db_table = 'shangjia_ri_tongji'
verbose_name = '商家每日统计' verbose_name = '商家每日统计'
unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录 unique_together = [['MerchantID', 'Date']]
indexes = [ indexes = [
models.Index(fields=['riqi', '-paifa_jine']), models.Index(fields=['Date', '-AssignedAmount']),
models.Index(fields=['riqi', '-jiesuan_jine']), models.Index(fields=['Date', '-SettledAmount']),
models.Index(fields=['riqi', '-paifa_dingdan_shu']), models.Index(fields=['Date', '-AssignedOrderCount']),
] ]
def __str__(self):
return f"商家{self.MerchantID} {self.Date}"
# ==================== 7. 打手每日统计 ====================
class PlayerDailyStats(QModel):
class DashouRiTongji(QModel): """打手每日统计表(原 DashouRiTongji"""
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID') PlayerID = models.CharField(
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 max_length=7, db_index=True, db_column='yonghuid', verbose_name='打手用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
# 接单统计 # 接单统计
jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量') AcceptedOrderTotal = models.IntegerField(
chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量') default=0, db_column='jiedan_zongliang', verbose_name='单总量')
CompletedOrderTotal = models.IntegerField(
default=0, db_column='chengjiao_zongliang', verbose_name='成交单总量')
# 金额统计 # 金额统计
jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额') AcceptedAmount = models.DecimalField(
chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额') max_digits=12, decimal_places=2, default=0.00,
db_column='jiedan_zonge', verbose_name='接单总金额')
CompletedAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='chengjiao_zonge', verbose_name='成交总金额')
# 退款统计 # 退款统计
tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量') RefundCount = models.IntegerField(
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') default=0, db_column='tuikuan_liang', verbose_name='退款订单量')
RefundAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='tuikuan_jine', verbose_name='退款总金额')
class Meta: class Meta:
db_table = 'dashou_ri_tongji' db_table = 'dashou_ri_tongji'
verbose_name = '打手每日统计' verbose_name = '打手每日统计'
unique_together = [['yonghuid', 'riqi']] unique_together = [['PlayerID', 'Date']]
indexes = [ indexes = [
models.Index(fields=['riqi', '-jiedan_zongliang']), models.Index(fields=['Date', '-AcceptedOrderTotal']),
models.Index(fields=['riqi', '-chengjiao_zongliang']), models.Index(fields=['Date', '-CompletedOrderTotal']),
models.Index(fields=['riqi', '-jiedan_zonge']), models.Index(fields=['Date', '-AcceptedAmount']),
models.Index(fields=['riqi', '-chengjiao_zonge']), models.Index(fields=['Date', '-CompletedAmount']),
] ]
def __str__(self):
return f"打手{self.PlayerID} {self.Date}"
class ZuzhangRiTongji(QModel): # ==================== 8. 组长每日统计 ====================
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID') class LeaderDailyStats(QModel):
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 """组长每日统计表(原 ZuzhangRiTongji"""
LeaderID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='组长用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数') InvitedManagerCount = models.IntegerField(
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)') default=0, db_column='yaoqing_guanshi_shu', verbose_name='邀请管事总数')
TotalIncome = models.DecimalField(
# 可选明细 max_digits=12, decimal_places=2, default=0.00,
fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额') db_column='shouru_zonge', verbose_name='收入总额(分佣)')
CommissionAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='fenyong_jine', verbose_name='分佣金额')
class Meta: class Meta:
db_table = 'zuzhang_ri_tongji' db_table = 'zuzhang_ri_tongji'
verbose_name = '组长每日统计' verbose_name = '组长每日统计'
unique_together = [['yonghuid', 'riqi']] unique_together = [['LeaderID', 'Date']]
indexes = [ indexes = [
models.Index(fields=['riqi', '-yaoqing_guanshi_shu']), models.Index(fields=['Date', '-InvitedManagerCount']),
models.Index(fields=['riqi', '-shouru_zonge']), models.Index(fields=['Date', '-TotalIncome']),
] ]
def __str__(self):
return f"组长{self.LeaderID} {self.Date}"
# ==================== 9. 管事每日统计 ====================
class ManagerDailyStats(QModel):
"""管事每日统计表(原 GuanshiRiTongji"""
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
class GuanshiRiTongji(QModel): InvitedPlayerCount = models.IntegerField(
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') default=0, db_column='yaoqing_dashou_shu', verbose_name='邀请打手总数')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 RechargedPlayerCount = models.IntegerField(
default=0, db_column='chongzhi_dashou_shu', verbose_name='充值打手数量')
yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数') TotalIncome = models.DecimalField(
chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量') max_digits=12, decimal_places=2, default=0.00,
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额') db_column='shouru_zonge', verbose_name='收入总额')
class Meta: class Meta:
db_table = 'guanshi_ri_tongji' db_table = 'guanshi_ri_tongji'
verbose_name = '管事每日统计' verbose_name = '管事每日统计'
unique_together = [['yonghuid', 'riqi']] unique_together = [['ManagerID', 'Date']]
indexes = [ indexes = [
models.Index(fields=['riqi', '-yaoqing_dashou_shu']), models.Index(fields=['Date', '-InvitedPlayerCount']),
models.Index(fields=['riqi', '-chongzhi_dashou_shu']), models.Index(fields=['Date', '-RechargedPlayerCount']),
models.Index(fields=['riqi', '-shouru_zonge']), models.Index(fields=['Date', '-TotalIncome']),
] ]
def __str__(self):
return f"管事{self.ManagerID} {self.Date}"
# ==================== 10. 管事续费每日统计 ====================
class ManagerRenewalDailyStats(QModel):
"""管事续费每日统计表(原 GuanshiXufeiRiTongji"""
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
class GuanshiXufeiRiTongji(QModel): RenewalTotal = models.IntegerField(
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') default=0, db_column='xufei_zongshu', verbose_name='今日续费总数')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 RenewalRevenue = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数) db_column='xufei_shouyi', verbose_name='续费收益金额')
xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额')
class Meta: class Meta:
db_table = 'guanshi_xufei_ri_tongji' db_table = 'guanshi_xufei_ri_tongji'
verbose_name = '管事续费每日统计' verbose_name = '管事续费每日统计'
unique_together = [['yonghuid', 'riqi']] unique_together = [['ManagerID', 'Date']]
indexes = [ indexes = [
models.Index(fields=['riqi', '-xufei_zongshu']), models.Index(fields=['Date', '-RenewalTotal']),
models.Index(fields=['riqi', '-xufei_shouyi']), models.Index(fields=['Date', '-RenewalRevenue']),
] ]
def __str__(self):
return f"管事续费{self.ManagerID} {self.Date}"
# models.py 中添加 # ==================== 11. 提现每日统计 ====================
class WithdrawalDailyStats(QModel):
class TixianRiTongji(QModel): """每日提现统计表(原 TixianRiTongji按身份统计当天提现总额"""
""" Date = models.DateField(
每日提现统计表(按身份统计当天提现总额) db_index=True, db_column='riqi', verbose_name='日期')
""" WithdrawalType = models.IntegerField(
riqi = models.DateField(db_index=True, verbose_name='日期')
leixing = models.IntegerField(
choices=[(1, '打手'), (2, '管事'), (3, '组长'), choices=[(1, '打手'), (2, '管事'), (3, '组长'),
(4, '考核官'), (5, '打手押金'), (6, '商家余额')], (4, '考核官'), (5, '打手押金'), (6, '商家余额')],
verbose_name='用户类型' db_column='leixing', verbose_name='用户类型')
) total_amount = models.DecimalField(
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)') max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
class Meta: class Meta:
db_table = 'tixian_ri_tongji' db_table = 'tixian_ri_tongji'
verbose_name = '每日提现统计' verbose_name = '每日提现统计'
unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录 unique_together = [['Date', 'WithdrawalType']]
indexes = [models.Index(fields=['riqi', 'leixing'])] indexes = [models.Index(fields=['Date', 'WithdrawalType'])]
def __str__(self): def __str__(self):
return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}" return f"{self.Date} - {self.get_WithdrawalType_display()} 提现总额: {self.total_amount}"
# ==================== 兼容别名(供逐步迁移) ====================
ShangjiaRiTongji = MerchantDailyStats
DashouRiTongji = PlayerDailyStats
ZuzhangRiTongji = LeaderDailyStats
GuanshiRiTongji = ManagerDailyStats
GuanshiXufeiRiTongji = ManagerRenewalDailyStats
TixianRiTongji = WithdrawalDailyStats

View File

@@ -5,8 +5,8 @@ from functools import lru_cache
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q from django.db.models import Q
from rest_framework.response import Response from rest_framework.response import Response
from .models import AbnormalUserLog, UserRole, Permission, DashouRiTongji, ShangjiaRiTongji, \ from .models import AbnormalUserLog, UserRole, Permission, PlayerDailyStats, MerchantDailyStats, \
GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji ManagerRenewalDailyStats, ManagerDailyStats, LeaderDailyStats, WithdrawalDailyStats
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
@@ -129,36 +129,36 @@ def update_dashou_daily_by_action(yonghuid, amount, action):
today = date.today() today = date.today()
with transaction.atomic(): with transaction.atomic():
stat, created = DashouRiTongji.objects.select_for_update().get_or_create( stat, created = PlayerDailyStats.objects.select_for_update().get_or_create(
yonghuid=yonghuid, PlayerID=yonghuid,
riqi=today, Date=today,
defaults={ defaults={
'yonghuid': yonghuid, 'PlayerID': yonghuid,
'riqi': today, 'Date': today,
'jiedan_zongliang': 0, 'AcceptedOrderTotal': 0,
'jiedan_zonge': Decimal('0.00'), 'AcceptedAmount': Decimal('0.00'),
'chengjiao_zongliang': 0, 'CompletedOrderTotal': 0,
'chengjiao_zonge': Decimal('0.00'), 'CompletedAmount': Decimal('0.00'),
'tuikuan_liang': 0, 'RefundCount': 0,
'tuikuan_jine': Decimal('0.00'), 'RefundAmount': Decimal('0.00'),
} }
) )
update_fields = {} update_fields = {}
if action == 1: # 接单 if action == 1: # 接单
update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1 update_fields['AcceptedOrderTotal'] = F('AcceptedOrderTotal') + 1
update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount update_fields['AcceptedAmount'] = F('AcceptedAmount') + amount
elif action == 2: # 成交 elif action == 2: # 成交
update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 update_fields['CompletedOrderTotal'] = F('CompletedOrderTotal') + 1
update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount update_fields['CompletedAmount'] = F('CompletedAmount') + amount
elif action == 3: # 退款 elif action == 3: # 退款
update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1 update_fields['RefundCount'] = F('RefundCount') + 1
update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount update_fields['RefundAmount'] = F('RefundAmount') + amount
else: else:
return # 无效行为 return # 无效行为
if update_fields: if update_fields:
DashouRiTongji.query.filter(id=stat.id).update(**update_fields) PlayerDailyStats.query.filter(id=stat.id).update(**update_fields)
def update_shangjia_daily(yonghuid, amount, action): def update_shangjia_daily(yonghuid, amount, action):
@@ -183,38 +183,38 @@ def update_shangjia_daily(yonghuid, amount, action):
with transaction.atomic(): with transaction.atomic():
# 获取或创建当天统计记录,同时加锁防止并发 # 获取或创建当天统计记录,同时加锁防止并发
stat, created = ShangjiaRiTongji.objects.select_for_update().get_or_create( stat, created = MerchantDailyStats.objects.select_for_update().get_or_create(
yonghuid=yonghuid, MerchantID=yonghuid,
riqi=today, Date=today,
defaults={ defaults={
'yonghuid': yonghuid, 'MerchantID': yonghuid,
'riqi': today, 'Date': today,
'paifa_dingdan_shu': 0, 'AssignedOrderCount': 0,
'paifa_jine': Decimal('0.00'), 'AssignedAmount': Decimal('0.00'),
'jiesuan_dingdan_shu': 0, 'SettledOrderCount': 0,
'jiesuan_jine': Decimal('0.00'), 'SettledAmount': Decimal('0.00'),
'tuikuan_dingdan_shu': 0, 'RefundOrderCount': 0,
'tuikuan_jine': Decimal('0.00'), 'RefundAmount': Decimal('0.00'),
} }
) )
# 构建本次更新的字段,使用 F 表达式进行原子累加 # 构建本次更新的字段,使用 F 表达式进行原子累加
update_fields = {} update_fields = {}
if action == 1: # 派发 if action == 1: # 派发
update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1 update_fields['AssignedOrderCount'] = F('AssignedOrderCount') + 1
update_fields['paifa_jine'] = F('paifa_jine') + amount update_fields['AssignedAmount'] = F('AssignedAmount') + amount
elif action == 2: # 结算(成交) elif action == 2: # 结算(成交)
update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1 update_fields['SettledOrderCount'] = F('SettledOrderCount') + 1
update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount update_fields['SettledAmount'] = F('SettledAmount') + amount
elif action == 3: # 退款 elif action == 3: # 退款
update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1 update_fields['RefundOrderCount'] = F('RefundOrderCount') + 1
update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount update_fields['RefundAmount'] = F('RefundAmount') + amount
else: else:
# 无效的行为类型,直接返回 # 无效的行为类型,直接返回
return return
# 执行原子更新 # 执行原子更新
ShangjiaRiTongji.query.filter(id=stat.id).update(**update_fields) MerchantDailyStats.query.filter(id=stat.id).update(**update_fields)
@@ -226,26 +226,26 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
today = date.today() today = date.today()
with transaction.atomic(): with transaction.atomic():
stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( stat, created = ManagerDailyStats.objects.select_for_update().get_or_create(
yonghuid=yonghuid, ManagerID=yonghuid,
riqi=today, Date=today,
defaults={ defaults={
'yonghuid': yonghuid, 'ManagerID': yonghuid,
'riqi': today, 'Date': today,
'yaoqing_dashou_shu': 0, 'InvitedPlayerCount': 0,
'chongzhi_dashou_shu': 0, 'RechargedPlayerCount': 0,
'shouru_zonge': Decimal('0.00'), 'TotalIncome': Decimal('0.00'),
} }
) )
if action == 1: if action == 1:
# 邀请打手:无论新创建还是已存在,直接 +1 # 邀请打手:无论新创建还是已存在,直接 +1
if created: if created:
stat.yaoqing_dashou_shu = 1 stat.InvitedPlayerCount = 1
stat.save() stat.save()
else: else:
GuanshiRiTongji.query.filter(id=stat.id).update( ManagerDailyStats.query.filter(id=stat.id).update(
yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1 InvitedPlayerCount=F('InvitedPlayerCount') + 1
) )
elif action == 2: elif action == 2:
@@ -253,13 +253,13 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
if amount <= 0: if amount <= 0:
return return
if created: if created:
stat.chongzhi_dashou_shu = 1 stat.RechargedPlayerCount = 1
stat.shouru_zonge = amount stat.TotalIncome = amount
stat.save() stat.save()
else: else:
GuanshiRiTongji.query.filter(id=stat.id).update( ManagerDailyStats.query.filter(id=stat.id).update(
chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1, RechargedPlayerCount=F('RechargedPlayerCount') + 1,
shouru_zonge=F('shouru_zonge') + amount TotalIncome=F('TotalIncome') + amount
) )
elif action in (3, 4): elif action in (3, 4):
@@ -267,70 +267,13 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
if amount <= 0: if amount <= 0:
return return
if created: if created:
stat.shouru_zonge = amount stat.TotalIncome = amount
stat.save() stat.save()
else: else:
GuanshiRiTongji.query.filter(id=stat.id).update( ManagerDailyStats.query.filter(id=stat.id).update(
shouru_zonge=F('shouru_zonge') + amount TotalIncome=F('TotalIncome') + amount
) )
'''def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
"""
根据操作行为更新管事每日统计(邀请打手、充值会员)
参数:
yonghuid: 管事用户ID
action: 操作类型
1 = 邀请打手(次数自动+1无需传其他参数
2 = 充值会员(需要传 amount 参数)
amount: 充值金额Decimal仅在 action=2 时有效
说明:
- 每次调用只代表一次操作,不会批量处理。
- 使用 select_for_update + F() 保证原子性和并发安全。
"""
today = date.today()
# 邀请打手:次数+1金额不变
if action == 1:
count_field = 'yaoqing_dashou_shu'
amount = Decimal('0.00')
# 充值会员:次数+1金额累加
elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
count_field = 'chongzhi_dashou_shu'
else:
return # 无效行为,直接返回
with transaction.atomic():
stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'yaoqing_dashou_shu': 0,
'chongzhi_dashou_shu': 0,
'shouru_zonge': Decimal('0.00'),
}
)
if created:
# 新记录直接赋初值
if action == 1:
stat.yaoqing_dashou_shu = 1
else:
# 已存在记录,使用 F 表达式原子累加次数
update_fields = {count_field: F(count_field) + 1}
if action == 2:
update_fields['shouru_zonge'] = F('shouru_zonge') + amount
GuanshiRiTongji.query.filter(id=stat.id).update(**update_fields)'''
def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')):
""" """
@@ -339,20 +282,20 @@ def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')):
""" """
today = date.today() today = date.today()
with transaction.atomic(): with transaction.atomic():
stat, created = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create( stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create(
yonghuid=yonghuid, ManagerID=yonghuid,
riqi=today, Date=today,
defaults={ defaults={
'yonghuid': yonghuid, 'ManagerID': yonghuid,
'riqi': today, 'Date': today,
'xufei_zongshu': 1, 'RenewalTotal': 1,
'xufei_shouyi': xufei_jine, 'RenewalRevenue': xufei_jine,
} }
) )
if not created: if not created:
GuanshiXufeiRiTongji.query.filter(id=stat.id).update( ManagerRenewalDailyStats.query.filter(id=stat.id).update(
xufei_zongshu=F('xufei_zongshu') + 1, RenewalTotal=F('RenewalTotal') + 1,
xufei_shouyi=F('xufei_shouyi') + xufei_jine RenewalRevenue=F('RenewalRevenue') + xufei_jine
) )
@@ -370,39 +313,39 @@ def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
today = date.today() today = date.today()
with transaction.atomic(): with transaction.atomic():
stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create( stat, created = LeaderDailyStats.objects.select_for_update().get_or_create(
yonghuid=yonghuid, LeaderID=yonghuid,
riqi=today, Date=today,
defaults={ defaults={
'yonghuid': yonghuid, 'LeaderID': yonghuid,
'riqi': today, 'Date': today,
'yaoqing_guanshi_shu': 0, 'InvitedManagerCount': 0,
'shouru_zonge': Decimal('0.00'), 'TotalIncome': Decimal('0.00'),
'fenyong_jine': Decimal('0.00'), 'CommissionAmount': Decimal('0.00'),
} }
) )
if created: if created:
# 新记录直接设置初始值 # 新记录直接设置初始值
if action == 1: if action == 1:
stat.yaoqing_guanshi_shu = 1 stat.InvitedManagerCount = 1
elif action == 2: elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00') amount = Decimal(str(amount)) if amount else Decimal('0.00')
stat.shouru_zonge = amount stat.TotalIncome = amount
stat.fenyong_jine = amount stat.CommissionAmount = amount
stat.save() stat.save()
else: else:
# 已存在记录,使用 F 表达式原子累加 # 已存在记录,使用 F 表达式原子累加
if action == 1: if action == 1:
ZuzhangRiTongji.query.filter(id=stat.id).update( LeaderDailyStats.query.filter(id=stat.id).update(
yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 InvitedManagerCount=F('InvitedManagerCount') + 1
) )
elif action == 2: elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00') amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount > 0: if amount > 0:
ZuzhangRiTongji.query.filter(id=stat.id).update( LeaderDailyStats.query.filter(id=stat.id).update(
shouru_zonge=F('shouru_zonge') + amount, TotalIncome=F('TotalIncome') + amount,
fenyong_jine=F('fenyong_jine') + amount CommissionAmount=F('CommissionAmount') + amount
) )
@@ -415,13 +358,13 @@ def update_tixian_daily_stat(leixing, amount):
""" """
today = date.today() today = date.today()
with transaction.atomic(): with transaction.atomic():
stat, created = TixianRiTongji.objects.select_for_update().get_or_create( stat, created = WithdrawalDailyStats.objects.select_for_update().get_or_create(
riqi=today, Date=today,
leixing=leixing, WithdrawalType=leixing,
defaults={'total_amount': amount} defaults={'total_amount': amount}
) )
if not created: if not created:
TixianRiTongji.query.filter(id=stat.id).update( WithdrawalDailyStats.query.filter(id=stat.id).update(
total_amount=F('total_amount') + amount total_amount=F('total_amount') + amount
) )

View File

@@ -52,7 +52,7 @@ from .utils import (
# models 集中导入 # models 集中导入
## backend ## backend
from backend.models import Role, Permission, RolePermission, UserRole, TixianRiTongji from backend.models import Role, Permission, RolePermission, UserRole, WithdrawalDailyStats
## users ## users
from users.models import ( from users.models import (
@@ -3329,10 +3329,10 @@ class GetWithdrawSettingsView(APIView):
# 4. 当日已提现总额(按提现类型 leixing 1~6 # 4. 当日已提现总额(按提现类型 leixing 1~6
today = date.today() today = date.today()
today_totals = {i: 0.0 for i in range(1, 7)} today_totals = {i: 0.0 for i in range(1, 7)}
records = TixianRiTongji.query.filter(riqi=today) records = WithdrawalDailyStats.query.filter(Date=today)
for rec in records: for rec in records:
if rec.leixing in today_totals: if rec.WithdrawalType in today_totals:
today_totals[rec.leixing] = float(rec.total_amount) today_totals[rec.WithdrawalType] = float(rec.total_amount)
# 🆕 获取提现模式自动1 / 手动2默认2 # 🆕 获取提现模式自动1 / 手动2默认2
config = WithdrawConfig.query.filter(id=1).first() config = WithdrawConfig.query.filter(id=1).first()
@@ -3358,7 +3358,7 @@ class UpdateWithdrawSettingsView(APIView):
- 修改管事相关需要5500b - 修改管事相关需要5500b
- 修改组长相关需要5500c - 修改组长相关需要5500c
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
后端不直接修改 TixianRiTongji,该表由提现流程自动累加。 后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。
POST /houtai/htxgtxsz POST /houtai/htxgtxsz
""" """
permission_classes = [] permission_classes = []
@@ -3491,7 +3491,7 @@ class UpdateWithdrawSettingsView(APIView):
defaults={'mode': int(withdraw_mode)} defaults={'mode': int(withdraw_mode)}
) )
# 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 # 注意:WithdrawalDailyStats 表由提现流程自动更新,此处不直接修改
return Response({'code': 0, 'msg': '设置修改成功'}) return Response({'code': 0, 'msg': '设置修改成功'})

View File

@@ -1,149 +1,240 @@
"""FluentQuery 兼容层查询构建器
提供 SQLAlchemy 风格的链式 OOP 查询接口,包装 Django QuerySet。
核心类:
- ``FluentQuery`` — 链式查询构建器,由 ``Model.query`` 返回
- ``Session`` — 简易会话管理add/commit/rollback
- ``_CompatFunc`` — 兼容层聚合函数func.sum / func.count 等)
- ``db`` — 全局数据库操作入口db.session / db.or_ / db.and_
典型用法::
from gvsdsdk.fluent import db, func, FQ
# 链式查询
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
# 字段表达式
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
# 聚合
Dingdan.query.aggregate(total=Sum('jine'))
# values / annotate
Dingdan.query.values('zhuangtai').annotate(cnt=Count('id'))
# 分页
Dingdan.query.paginate(page=1, per_page=20)
# 兼容层聚合函数
func.sum(Dingdan.jine)
func.count(Dingdan.id)
func.year(Dingdan.create_time)
"""
from __future__ import annotations from __future__ import annotations
import logging
import threading import threading
import functools from typing import Any, Dict, Generic, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Union
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, overload
from django.db import connection as default_db_connection, models, transaction from django.db import connection as default_db_connection, models, transaction
from django.db.models import Q, F, Count, Sum, Avg, Min, Max, Subquery, OuterRef, Case, When, Value from django.db.models import Q, F, Count, Sum, Avg, Min, Max, Subquery, OuterRef, Case, When, Value
logger = logging.getLogger(__name__)
_local = threading.local() _local = threading.local()
#: 模型类型变量,用于 FluentQuery[_M] 泛型参数
_M = TypeVar('_M', bound=models.Model)
def _get_field_expression_cls(): def _get_field_expression_cls():
"""延迟导入,避免 fluent ↔ model_base 循环依赖""" """延迟导入 FieldExpression,避免 fluent ↔ model_base 循环依赖"""
from gvsdsdk.model_base import FieldExpression from gvsdsdk.model_base import FieldExpression
return FieldExpression return FieldExpression
# ---------------------------------------------------------------------------
# 兼容层聚合函数
# ---------------------------------------------------------------------------
class _AggregateExpr: class _AggregateExpr:
def __init__(self, model, aggregate): """聚合表达式包装,关联模型类和 Django Aggregate 对象。"""
def __init__(self, model: Optional[Type[models.Model]], aggregate: models.Aggregate) -> None:
self.model = model self.model = model
self.aggregate = aggregate self.aggregate = aggregate
class _ExtractExpr: class _ExtractExpr:
def __init__(self, field_name, lookup_type): """日期提取表达式year/month/day用于 ``func.year(field) == 2024`` 等。"""
def __init__(self, field_name: str, lookup_type: str) -> None:
self.field_name = field_name self.field_name = field_name
self.lookup_type = lookup_type self.lookup_type = lookup_type
def __eq__(self, other): def __eq__(self, other) -> Q: # type: ignore[override]
return Q(**{f'{self.field_name}__{self.lookup_type}': other}) return Q(**{f'{self.field_name}__{self.lookup_type}': other})
def __and__(self, other): def __and__(self, other) -> Q:
if isinstance(other, Q): if isinstance(other, Q):
return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other
return self.__eq__(other) return self.__eq__(other)
class _CompatFunc: class _CompatFunc:
def sum(self, field_expr): """兼容层聚合函数集合,提供 SQLAlchemy 风格的 ``func.sum()`` / ``func.count()`` 等。
if hasattr(field_expr, '_django_name'):
agg = Sum(field_expr._django_name) 用法::
elif hasattr(field_expr, 'field_name'):
agg = Sum(field_expr.field_name) from gvsdsdk.fluent import func
else:
agg = Sum(str(field_expr)) func.sum(Model.jine) → Sum('jine')
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None func.count(Model.id) → Count('id')
func.max(Model.jine) → Max('jine')
func.min(Model.jine) → Min('jine')
func.avg(Model.jine) → Avg('jine')
func.year(Model.riqi) → _ExtractExpr('riqi', 'year')
func.month(Model.riqi) → _ExtractExpr('riqi', 'month')
func.day(Model.riqi) → _ExtractExpr('riqi', 'day')
"""
def sum(self, field_expr) -> Union[_AggregateExpr, Sum]:
"""求和聚合。"""
name = self._resolve_name(field_expr)
agg = Sum(name)
model = getattr(field_expr, 'model_class', None)
return _AggregateExpr(model, agg) if model else agg return _AggregateExpr(model, agg) if model else agg
def count(self, field_expr=None): def count(self, field_expr=None) -> Union[_AggregateExpr, Count]:
"""计数聚合,无参数时等价于 COUNT(*)。"""
if field_expr is None: if field_expr is None:
return Count('*') return Count('*')
if hasattr(field_expr, '_django_name'): name = self._resolve_name(field_expr)
agg = Count(field_expr._django_name) agg = Count(name)
else: model = getattr(field_expr, 'model_class', None)
agg = Count(str(field_expr))
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
return _AggregateExpr(model, agg) if model else agg return _AggregateExpr(model, agg) if model else agg
def max(self, field_expr): def max(self, field_expr) -> Union[_AggregateExpr, Max]:
if hasattr(field_expr, '_django_name'): """最大值聚合。"""
agg = Max(field_expr._django_name) name = self._resolve_name(field_expr)
else: agg = Max(name)
agg = Max(str(field_expr)) model = getattr(field_expr, 'model_class', None)
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
return _AggregateExpr(model, agg) if model else agg return _AggregateExpr(model, agg) if model else agg
def min(self, field_expr): def min(self, field_expr) -> Union[_AggregateExpr, Min]:
if hasattr(field_expr, '_django_name'): """最小值聚合。"""
agg = Min(field_expr._django_name) name = self._resolve_name(field_expr)
else: agg = Min(name)
agg = Min(str(field_expr)) model = getattr(field_expr, 'model_class', None)
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
return _AggregateExpr(model, agg) if model else agg return _AggregateExpr(model, agg) if model else agg
def avg(self, field_expr): def avg(self, field_expr) -> Union[_AggregateExpr, Avg]:
if hasattr(field_expr, '_django_name'): """平均值聚合。"""
agg = Avg(field_expr._django_name) name = self._resolve_name(field_expr)
else: agg = Avg(name)
agg = Avg(str(field_expr)) model = getattr(field_expr, 'model_class', None)
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
return _AggregateExpr(model, agg) if model else agg return _AggregateExpr(model, agg) if model else agg
def year(self, field_expr): def year(self, field_expr) -> _ExtractExpr:
if hasattr(field_expr, '_django_name'): """提取年份,用于 ``func.year(Model.date) == 2024``。"""
return _ExtractExpr(field_expr._django_name, 'year') return _ExtractExpr(self._resolve_name(field_expr), 'year')
return _ExtractExpr(str(field_expr), 'year')
def month(self, field_expr): def month(self, field_expr) -> _ExtractExpr:
if hasattr(field_expr, '_django_name'): """提取月份。"""
return _ExtractExpr(field_expr._django_name, 'month') return _ExtractExpr(self._resolve_name(field_expr), 'month')
return _ExtractExpr(str(field_expr), 'month')
def day(self, field_expr): def day(self, field_expr) -> _ExtractExpr:
"""提取日期。"""
return _ExtractExpr(self._resolve_name(field_expr), 'day')
@staticmethod
def _resolve_name(field_expr) -> str:
"""从 FieldExpression 或字符串解析 Django 字段名。"""
if hasattr(field_expr, '_django_name'): if hasattr(field_expr, '_django_name'):
return _ExtractExpr(field_expr._django_name, 'day') return field_expr._django_name
return _ExtractExpr(str(field_expr), 'day') if hasattr(field_expr, 'field_name'):
return field_expr.field_name
return str(field_expr)
func = _CompatFunc() func = _CompatFunc()
# ---------------------------------------------------------------------------
# DELETE 语句构建器
# ---------------------------------------------------------------------------
class _DeleteStmt: class _DeleteStmt:
def __init__(self, model_class, conditions=None): """DELETE 语句构建器,支持 ``Model.query.filter(...).delete()`` 风格。"""
def __init__(self, model_class: Type[models.Model], conditions: Optional[Q] = None) -> None:
self.model_class = model_class self.model_class = model_class
self.conditions = conditions or Q() self.conditions = conditions or Q()
def where(self, *args, **kwargs): def where(self, *args: Q, **kwargs: Any) -> _DeleteStmt:
"""添加 WHERE 条件。"""
new_conditions = Q() new_conditions = Q()
for a in args: for a in args:
if isinstance(a, Q): if isinstance(a, Q):
new_conditions &= a new_conditions &= a
if kwargs:
new_conditions &= Q(**kwargs)
self.conditions = new_conditions self.conditions = new_conditions
return self return self
def execute(self): def execute(self) -> Tuple[int, Dict[str, int]]:
"""执行删除,返回 (删除数, {表名: 删除数})。"""
if self.model_class and self.conditions: if self.model_class and self.conditions:
self.model_class.objects.filter(self.conditions).delete() return self.model_class.objects.filter(self.conditions).delete()
elif self.model_class: elif self.model_class:
self.model_class.objects.all().delete() return self.model_class.objects.all().delete()
return (0, {})
def _get_session(): # ---------------------------------------------------------------------------
# 会话与缓存
# ---------------------------------------------------------------------------
def _get_session() -> Session:
"""获取当前线程的 Session 实例。"""
if not hasattr(_local, 'db_session'): if not hasattr(_local, 'db_session'):
_local.db_session = Session() _local.db_session = Session()
return _local.db_session return _local.db_session
def _get_cache(): def _get_cache() -> Dict:
"""获取当前线程的查询缓存。"""
if not hasattr(_local, 'query_cache'): if not hasattr(_local, 'query_cache'):
_local.query_cache = {} _local.query_cache = {}
return _local.query_cache return _local.query_cache
def _invalidate_cache(): def _invalidate_cache() -> None:
"""清除当前线程的查询缓存。"""
if hasattr(_local, 'query_cache'): if hasattr(_local, 'query_cache'):
_local.query_cache.clear() _local.query_cache.clear()
# ---------------------------------------------------------------------------
# 关联字段访问器
# ---------------------------------------------------------------------------
class RelationAccessor: class RelationAccessor:
def __init__(self, model_class, prefix=''): """关联字段访问器,支持 ``Model.relation.field`` 跨表字段表达式。
用法::
# 自动解析关联字段
Dingdan.query.filter(Dingdan.shangjia.nicheng == '测试')
"""
def __init__(self, model_class: Type[models.Model], prefix: str = '') -> None:
self._model = model_class self._model = model_class
self._prefix = prefix self._prefix = prefix
def __getattr__(self, name): def __getattr__(self, name: str):
if name.startswith('_'): if name.startswith('_'):
return object.__getattribute__(self, name) return object.__getattribute__(self, name)
try: try:
@@ -163,18 +254,26 @@ class RelationAccessor:
return RelationAccessor(rel.related_model, full_prefix) return RelationAccessor(rel.related_model, full_prefix)
return _get_field_expression_cls()(self._model, name) return _get_field_expression_cls()(self._model, name)
def __repr__(self) -> str:
return f'RelationAccessor({self._model.__name__}, prefix={self._prefix!r})'
# ---------------------------------------------------------------------------
# Values 结果集
# ---------------------------------------------------------------------------
class _ValuesResult: class _ValuesResult:
"""包装 Django ValuesQuerySet支持链式调用。
底层使用 Django ``.values()`` 返回字典,保持与 Django 行为一致。
额外提供 ``.all()`` 返回字典列表,``.first()`` 返回字典或 None
``.scalar()`` 返回单字段场景的标量值。
""" """
包装 Django ValuesQuerySet支持链式调用。
底层使用 Django .values() 返回字典,保持与 Django 行为一致。
额外提供 .all() 返回字典列表,.first() 返回字典或 None
.scalar() 返回单字段场景的标量值。
"""
def __init__(self, qs: models.QuerySet, fields: List[str], original_fields: Tuple) -> None: def __init__(self, qs: models.QuerySet, fields: List[str], original_fields: Tuple) -> None:
self._qs: models.QuerySet = qs # Django ValuesQuerySet返回字典 self._qs: models.QuerySet = qs
self._fields: List[str] = fields # 转换后的字段名列表(传给 Django .values() 的) self._fields: List[str] = fields
self._original_fields: Tuple = original_fields # 原始传入的字段(用于判断单字段场景) self._original_fields: Tuple = original_fields
@property @property
def query(self) -> Any: def query(self) -> Any:
@@ -202,12 +301,15 @@ class _ValuesResult:
return self return self
def first(self) -> Optional[Dict[str, Any]]: def first(self) -> Optional[Dict[str, Any]]:
"""返回第一条记录(字典)或 None。"""
return self._qs.first() return self._qs.first()
def all(self) -> List[Dict[str, Any]]: def all(self) -> List[Dict[str, Any]]:
"""返回所有记录的字典列表。"""
return list(self._qs) return list(self._qs)
def scalar(self) -> Any: def scalar(self) -> Any:
"""返回单字段场景的标量值。"""
row = self._qs.first() row = self._qs.first()
if row is None: if row is None:
return None return None
@@ -237,11 +339,18 @@ class _ValuesResult:
def __bool__(self) -> bool: def __bool__(self) -> bool:
return self._qs.exists() return self._qs.exists()
def __repr__(self) -> str:
return f'_ValuesResult(fields={self._fields}, count={self.count()})'
class FluentQuery:
# ---------------------------------------------------------------------------
# FluentQuery 核心查询构建器
# ---------------------------------------------------------------------------
class FluentQuery(Generic[_M]):
"""QModel 兼容层查询构建器,提供 SQLAlchemy 风格的链式 OOP 查询接口。 """QModel 兼容层查询构建器,提供 SQLAlchemy 风格的链式 OOP 查询接口。
法示例:: 由 ``Model.query`` 返回,支持链式调用::
# 基础查询 # 基础查询
Dingdan.query.filter(zhuangtai=8).all() Dingdan.query.filter(zhuangtai=8).all()
@@ -249,7 +358,7 @@ class FluentQuery:
# 链式过滤 # 链式过滤
Dingdan.query.filter(jine__gt=100).order_by('-create_time').limit(10) Dingdan.query.filter(jine__gt=100).order_by('-create_time').limit(10)
# 字段表达式(通过 Model.field 自动获取 FieldExpression # 字段表达式
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
# 聚合 # 聚合
@@ -260,10 +369,12 @@ class FluentQuery:
# 分页 # 分页
Dingdan.query.paginate(page=1, per_page=20) Dingdan.query.paginate(page=1, per_page=20)
泛型参数 ``_M`` 携带模型类型,使 IDE 能推断链式调用返回的模型实例类型。
""" """
def __init__(self, model_class: Type[models.Model]) -> None: def __init__(self, model_class: Type[_M]) -> None:
self._model: Type[models.Model] = model_class self._model: Type[_M] = model_class
self._qs: models.QuerySet = model_class.objects.all() self._qs: models.QuerySet = model_class.objects.all()
self._joined_models: List[Type[models.Model]] = [] self._joined_models: List[Type[models.Model]] = []
self._cache_enabled: bool = True self._cache_enabled: bool = True
@@ -273,7 +384,13 @@ class FluentQuery:
"""暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。""" """暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。"""
return self._qs.query return self._qs.query
def _clone(self) -> FluentQuery: @property
def model(self) -> Type[_M]:
"""返回关联的模型类。"""
return self._model
def _clone(self) -> FluentQuery[_M]:
"""克隆当前查询状态。"""
q = FluentQuery(self._model) q = FluentQuery(self._model)
q._qs = self._qs q._qs = self._qs
q._joined_models = list(self._joined_models) q._joined_models = list(self._joined_models)
@@ -281,6 +398,7 @@ class FluentQuery:
return q return q
def _cache_key(self, method: str) -> Tuple[Any, str, Any]: def _cache_key(self, method: str) -> Tuple[Any, str, Any]:
"""生成缓存键。"""
try: try:
query_str = str(self._qs.query) query_str = str(self._qs.query)
except Exception: except Exception:
@@ -288,6 +406,7 @@ class FluentQuery:
return (self._model, method, query_str) return (self._model, method, query_str)
def _remap_q(self, q: Q) -> Q: def _remap_q(self, q: Q) -> Q:
"""重映射 Q 对象中的字段名(用于 join 跨表查询)。"""
new_children: List[Any] = [] new_children: List[Any] = []
for child in q.children: for child in q.children:
if isinstance(child, Q): if isinstance(child, Q):
@@ -306,6 +425,7 @@ class FluentQuery:
return q return q
def _remap_key(self, key: str) -> str: def _remap_key(self, key: str) -> str:
"""重映射字段名(用于 join 跨表查询自动添加关联前缀)。"""
parts = key.split('__') parts = key.split('__')
field_name = parts[0] field_name = parts[0]
try: try:
@@ -330,7 +450,9 @@ class FluentQuery:
pass pass
return key return key
def filter(self, *args: Any, **kwargs: Any) -> FluentQuery: # ---- 过滤 ----
def filter(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]:
"""过滤查询,支持 Q 对象和关键字参数,返回 self 以支持链式调用。""" """过滤查询,支持 Q 对象和关键字参数,返回 self 以支持链式调用。"""
converted_args: List[Any] = [] converted_args: List[Any] = []
for arg in args: for arg in args:
@@ -341,7 +463,7 @@ class FluentQuery:
self._qs = self._qs.filter(*converted_args, **kwargs) self._qs = self._qs.filter(*converted_args, **kwargs)
return self return self
def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery: def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]:
"""排除查询,返回 self 以支持链式调用。""" """排除查询,返回 self 以支持链式调用。"""
converted_args: List[Any] = [] converted_args: List[Any] = []
for arg in args: for arg in args:
@@ -352,7 +474,9 @@ class FluentQuery:
self._qs = self._qs.exclude(*converted_args, **kwargs) self._qs = self._qs.exclude(*converted_args, **kwargs)
return self return self
def order_by(self, *args: Any) -> FluentQuery: # ---- 排序 ----
def order_by(self, *args: Any) -> FluentQuery[_M]:
"""排序,支持字符串和 FieldExpression返回 self 以支持链式调用。""" """排序,支持字符串和 FieldExpression返回 self 以支持链式调用。"""
converted: List[str] = [] converted: List[str] = []
for a in args: for a in args:
@@ -365,7 +489,9 @@ class FluentQuery:
self._qs = self._qs.order_by(*converted) self._qs = self._qs.order_by(*converted)
return self return self
def first(self) -> Optional[models.Model]: # ---- 获取记录 ----
def first(self) -> Optional[_M]:
"""返回第一条记录或 None。""" """返回第一条记录或 None。"""
if self._cache_enabled: if self._cache_enabled:
cache = _get_cache() cache = _get_cache()
@@ -379,13 +505,26 @@ class FluentQuery:
_get_cache()[self._cache_key('first')] = obj _get_cache()[self._cache_key('first')] = obj
return obj return obj
def all(self) -> FluentQuery: def last(self) -> Optional[_M]:
"""返回最后一条记录或 None。"""
return self._qs.last()
def get(self, *args: Any, **kwargs: Any) -> _M:
"""获取唯一匹配记录,不存在或多个时抛异常。"""
if args:
obj = self._qs.filter(*args).get(**kwargs)
else:
obj = self._qs.get(**kwargs)
_get_session().track(obj)
return obj
def all(self) -> FluentQuery[_M]:
"""返回 self 以支持链式调用(与 Django QuerySet.all() 一致)。 """返回 self 以支持链式调用(与 Django QuerySet.all() 一致)。
如需获取列表,使用 .to_list() 或 list()。""" 如需获取列表,使用 ``.to_list()````list()``"""
self._qs = self._qs.all() self._qs = self._qs.all()
return self return self
def to_list(self) -> List[models.Model]: def to_list(self) -> List[_M]:
"""执行查询并返回模型实例列表。""" """执行查询并返回模型实例列表。"""
if self._cache_enabled: if self._cache_enabled:
cache = _get_cache() cache = _get_cache()
@@ -412,93 +551,37 @@ class FluentQuery:
_get_cache()[self._cache_key('count')] = result _get_cache()[self._cache_key('count')] = result
return result return result
def get(self, *args: Any, **kwargs: Any) -> models.Model: def exists(self) -> bool:
"""获取唯一匹配记录,不存在或多个时抛异常""" """判断是否存在匹配记录"""
if args: return self._qs.exists()
obj = self._qs.filter(*args).get(**kwargs)
else:
obj = self._qs.get(**kwargs)
_get_session().track(obj)
return obj
def create(self, **kwargs: Any) -> models.Model: # ---- 创建/更新/删除 ----
def create(self, **kwargs: Any) -> _M:
"""创建并保存新记录。""" """创建并保存新记录。"""
obj = self._model.objects.create(**kwargs) obj = self._model.objects.create(**kwargs)
_invalidate_cache() _invalidate_cache()
return obj return obj
def get_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]: def get_or_create(self, defaults: Optional[Dict[str, Any]] = None,
"""查询或创建记录,返回 (obj, created) 元组。""" **kwargs: Any) -> Tuple[_M, bool]:
"""查询或创建记录,返回 ``(obj, created)`` 元组。"""
obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs) obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs)
if created: if created:
_invalidate_cache() _invalidate_cache()
_get_session().track(obj) _get_session().track(obj)
return obj, created return obj, created
def update_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]: def update_or_create(self, defaults: Optional[Dict[str, Any]] = None,
"""更新或创建记录,返回 (obj, created) 元组。""" **kwargs: Any) -> Tuple[_M, bool]:
"""更新或创建记录,返回 ``(obj, created)`` 元组。"""
obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs) obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs)
_invalidate_cache() _invalidate_cache()
_get_session().track(obj) _get_session().track(obj)
return obj, created return obj, created
def none(self) -> FluentQuery: def bulk_create(self, objs: List[_M], batch_size: Optional[int] = None,
"""返回空结果集,返回 self 以支持链式调用。""" ignore_conflicts: bool = False) -> List[_M]:
self._qs = self._qs.none()
return self
def last(self) -> Optional[models.Model]:
"""返回最后一条记录或 None。"""
return self._qs.last()
def limit(self, n: int) -> FluentQuery:
"""限制返回记录数,返回 self 以支持链式调用。"""
self._qs = self._qs[:n]
return self
def offset(self, n: int) -> FluentQuery:
"""跳过前 n 条记录,返回 self 以支持链式调用。"""
self._qs = self._qs[n:]
return self
def join(self, *args: Type[models.Model]) -> FluentQuery:
"""关联模型,用于跨表查询自动映射字段,返回 self 以支持链式调用。"""
for arg in args:
if isinstance(arg, type) and issubclass(arg, models.Model):
if arg not in self._joined_models:
self._joined_models.append(arg)
return self
def select_related(self, *fields: str) -> FluentQuery:
"""外键关联查询优化,返回 self 以支持链式调用。"""
self._qs = self._qs.select_related(*fields)
return self
def prefetch_related(self, *fields: str) -> FluentQuery:
"""多对多/反向关联查询优化,返回 self 以支持链式调用。"""
self._qs = self._qs.prefetch_related(*fields)
return self
def only(self, *fields: str) -> FluentQuery:
"""只加载指定字段,返回 self 以支持链式调用。"""
self._qs = self._qs.only(*fields)
return self
def defer(self, *fields: str) -> FluentQuery:
"""延迟加载指定字段,返回 self 以支持链式调用。"""
self._qs = self._qs.defer(*fields)
return self
def select_for_update(self, nowait: bool = False, skip_locked: bool = False,
of: Tuple = (), no_key: bool = False) -> FluentQuery:
"""行级锁,返回 self 以支持链式调用。"""
self._qs = self._qs.select_for_update(
nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key
)
return self
def bulk_create(self, objs: List[models.Model], batch_size: Optional[int] = None,
ignore_conflicts: bool = False) -> List[models.Model]:
"""批量创建记录。""" """批量创建记录。"""
result = self._model.objects.bulk_create( result = self._model.objects.bulk_create(
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
@@ -506,7 +589,100 @@ class FluentQuery:
_invalidate_cache() _invalidate_cache()
return result return result
def annotate(self, **kwargs: Any) -> FluentQuery: def bulk_update(self, objs: List[_M], fields: List[str],
batch_size: Optional[int] = None) -> int:
"""批量更新记录,返回受影响行数。"""
result = self._model.objects.bulk_update(objs, fields, batch_size=batch_size)
_invalidate_cache()
return result
def update(self, **kwargs: Any) -> int:
"""批量更新匹配记录,返回受影响行数。"""
result = self._qs.update(**kwargs)
_invalidate_cache()
return result
def delete(self) -> Tuple[int, Dict[str, int]]:
"""批量删除匹配记录,返回 ``(删除数, {表名: 删除数})``。"""
result = self._qs.delete()
_invalidate_cache()
return result
# ---- 查询修饰 ----
def none(self) -> FluentQuery[_M]:
"""返回空结果集,返回 self 以支持链式调用。"""
self._qs = self._qs.none()
return self
def limit(self, n: int) -> FluentQuery[_M]:
"""限制返回记录数,返回 self 以支持链式调用。"""
self._qs = self._qs[:n]
return self
def offset(self, n: int) -> FluentQuery[_M]:
"""跳过前 n 条记录,返回 self 以支持链式调用。"""
self._qs = self._qs[n:]
return self
def join(self, *args: Type[models.Model]) -> FluentQuery[_M]:
"""关联模型,用于跨表查询自动映射字段,返回 self 以支持链式调用。"""
for arg in args:
if isinstance(arg, type) and issubclass(arg, models.Model):
if arg not in self._joined_models:
self._joined_models.append(arg)
return self
def select_related(self, *fields: str) -> FluentQuery[_M]:
"""外键关联查询优化,返回 self 以支持链式调用。"""
self._qs = self._qs.select_related(*fields)
return self
def prefetch_related(self, *fields: str) -> FluentQuery[_M]:
"""多对多/反向关联查询优化,返回 self 以支持链式调用。"""
self._qs = self._qs.prefetch_related(*fields)
return self
def only(self, *fields: str) -> FluentQuery[_M]:
"""只加载指定字段,返回 self 以支持链式调用。"""
self._qs = self._qs.only(*fields)
return self
def defer(self, *fields: str) -> FluentQuery[_M]:
"""延迟加载指定字段,返回 self 以支持链式调用。"""
self._qs = self._qs.defer(*fields)
return self
def select_for_update(self, nowait: bool = False, skip_locked: bool = False,
of: Tuple = (), no_key: bool = False) -> FluentQuery[_M]:
"""行级锁,返回 self 以支持链式调用。"""
self._qs = self._qs.select_for_update(
nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key
)
return self
def distinct(self) -> FluentQuery[_M]:
"""去重查询,返回 self 以支持链式调用。"""
self._qs = self._qs.distinct()
return self
def using(self, alias: str) -> FluentQuery[_M]:
"""指定数据库别名,返回 self 以支持链式调用。"""
self._qs = self._qs.using(alias)
return self
def no_cache(self) -> FluentQuery[_M]:
"""禁用查询缓存,返回 self 以支持链式调用。"""
self._cache_enabled = False
return self
def params(self, **kwargs: Any) -> FluentQuery[_M]:
"""参数占位SQLAlchemy 兼容),返回 self 以支持链式调用。"""
return self
# ---- 聚合与注解 ----
def annotate(self, **kwargs: Any) -> FluentQuery[_M]:
"""注解查询,添加聚合/计算字段,返回 self 以支持链式调用。""" """注解查询,添加聚合/计算字段,返回 self 以支持链式调用。"""
self._qs = self._qs.annotate(**kwargs) self._qs = self._qs.annotate(**kwargs)
return self return self
@@ -518,7 +694,6 @@ class FluentQuery:
def values(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult: def values(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult:
"""返回指定字段的字典结果,支持链式调用。""" """返回指定字段的字典结果,支持链式调用。"""
if not fields: if not fields:
# 无参数:返回 Django .values()(所有字段字典)
return _ValuesResult(self._qs.values(), [], fields) return _ValuesResult(self._qs.values(), [], fields)
annotations: Dict[str, models.Aggregate] = {} annotations: Dict[str, models.Aggregate] = {}
@@ -538,7 +713,6 @@ class FluentQuery:
qs = self._qs qs = self._qs
if annotations: if annotations:
qs = qs.annotate(**annotations) qs = qs.annotate(**annotations)
# 使用 Django 原生 .values() 返回字典
qs = qs.values(*converted) qs = qs.values(*converted)
return _ValuesResult(qs, converted, fields) return _ValuesResult(qs, converted, fields)
@@ -546,38 +720,16 @@ class FluentQuery:
"""values() 的别名SQLAlchemy 风格。""" """values() 的别名SQLAlchemy 风格。"""
return self.values(*fields) return self.values(*fields)
def params(self, **kwargs: Any) -> FluentQuery:
"""参数占位SQLAlchemy 兼容),返回 self 以支持链式调用。"""
return self
def values_list(self, *fields: str, flat: bool = False) -> Any: def values_list(self, *fields: str, flat: bool = False) -> Any:
"""返回指定字段的元组列表。""" """返回指定字段的元组列表。"""
return self._qs.values_list(*fields, flat=flat) return self._qs.values_list(*fields, flat=flat)
def update(self, **kwargs: Any) -> int: # ---- 分页 ----
"""批量更新匹配记录,返回受影响行数。"""
result = self._qs.update(**kwargs)
_invalidate_cache()
return result
def delete(self) -> Tuple[int, Dict[str, int]]: def paginate(self, page: int = 1, per_page: int = 20,
"""批量删除匹配记录,返回 (删除数, {表名: 删除数})。""" error_out: bool = False) -> Any:
result = self._qs.delete()
_invalidate_cache()
return result
def exists(self) -> bool:
"""判断是否存在匹配记录。"""
return self._qs.exists()
def distinct(self) -> FluentQuery:
"""去重查询,返回 self 以支持链式调用。"""
self._qs = self._qs.distinct()
return self
def paginate(self, page: int = 1, per_page: int = 20, error_out: bool = False) -> Any:
"""分页查询,返回 Page 对象(含 items, total, pages 等属性)。""" """分页查询,返回 Page 对象(含 items, total, pages 等属性)。"""
from django.core.paginator import Paginator from django.core.paginator import Paginator, EmptyPage
if not self._qs.ordered: if not self._qs.ordered:
meta = getattr(self._model, '_meta', None) meta = getattr(self._model, '_meta', None)
if meta and meta.pk: if meta and meta.pk:
@@ -585,7 +737,7 @@ class FluentQuery:
paginator = Paginator(self._qs, per_page) paginator = Paginator(self._qs, per_page)
try: try:
page_obj = paginator.page(page) page_obj = paginator.page(page)
except Exception: except EmptyPage:
page_obj = paginator.page(1) page_obj = paginator.page(1)
page_obj.total = paginator.count page_obj.total = paginator.count
page_obj.items = page_obj.object_list page_obj.items = page_obj.object_list
@@ -598,26 +750,17 @@ class FluentQuery:
page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None
return page_obj return page_obj
def no_cache(self) -> FluentQuery: # ---- 魔术方法 ----
"""禁用查询缓存,返回 self 以支持链式调用。"""
self._cache_enabled = False
return self
def using(self, alias: str) -> FluentQuery:
"""指定数据库别名,返回 self 以支持链式调用。"""
self._qs = self._qs.using(alias)
return self
def __getitem__(self, key: Union[int, slice]) -> Any: def __getitem__(self, key: Union[int, slice]) -> Any:
"""支持切片 [start:end] 和索引 [n]。切片返回 self索引返回模型实例。""" """支持切片 ``[start:end]`` 和索引 ``[n]``。切片返回 self索引返回模型实例。"""
if isinstance(key, slice): if isinstance(key, slice):
self._qs = self._qs[key] self._qs = self._qs[key]
return self return self
else: else:
# 单个索引,返回模型实例
return self._qs[key] return self._qs[key]
def __iter__(self) -> Iterator[models.Model]: def __iter__(self) -> Iterator[_M]:
return iter(self._qs) return iter(self._qs)
def __len__(self) -> int: def __len__(self) -> int:
@@ -630,25 +773,43 @@ class FluentQuery:
return f'FluentQuery({self._model.__name__})' return f'FluentQuery({self._model.__name__})'
class Session: # ---------------------------------------------------------------------------
def __init__(self): # Session 会话管理
self._pending_adds = [] # ---------------------------------------------------------------------------
self._pending_deletes = []
self._tracked = set()
def add(self, obj): class Session:
"""简易会话管理器,支持 add/delete/commit/rollback 操作。
用法::
session = db.session
session.add(obj)
session.commit()
"""
def __init__(self) -> None:
self._pending_adds: List[models.Model] = []
self._pending_deletes: List[models.Model] = []
self._tracked: set = set()
def add(self, obj: models.Model) -> None:
"""添加对象到待保存列表。"""
self._pending_adds.append(obj) self._pending_adds.append(obj)
def delete(self, obj): def delete(self, obj: models.Model) -> None:
"""添加对象到待删除列表。"""
self._pending_deletes.append(obj) self._pending_deletes.append(obj)
self._tracked.discard(obj) self._tracked.discard(obj)
def track(self, obj): def track(self, obj: models.Model) -> None:
"""跟踪对象变更commit 时自动 save"""
if isinstance(obj, models.Model): if isinstance(obj, models.Model):
self._tracked.add(obj) self._tracked.add(obj)
def commit(self): def commit(self) -> None:
delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None} """提交所有待处理操作(删除 → 新增 → 更新)。"""
delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes
if obj.pk is not None}
with transaction.atomic(): with transaction.atomic():
for obj in self._pending_deletes: for obj in self._pending_deletes:
obj.delete() obj.delete()
@@ -660,21 +821,23 @@ class Session:
try: try:
obj.save() obj.save()
except Exception: except Exception:
pass logger.debug('Session.commit: skip tracked obj %s', obj, exc_info=True)
self._pending_adds.clear() self._pending_adds.clear()
self._pending_deletes.clear() self._pending_deletes.clear()
self._tracked.clear() self._tracked.clear()
_invalidate_cache() _invalidate_cache()
def rollback(self): def rollback(self) -> None:
"""回滚所有待处理操作。"""
self._pending_adds.clear() self._pending_adds.clear()
self._pending_deletes.clear() self._pending_deletes.clear()
self._tracked.clear() self._tracked.clear()
_invalidate_cache() _invalidate_cache()
def query(self, *args): def query(self, *args) -> Optional[FluentQuery[Any]]:
model = None """创建查询构建器,支持传入模型类或聚合表达式。"""
aggregates = [] model: Optional[Type[models.Model]] = None
aggregates: List = []
for a in args: for a in args:
if isinstance(a, type) and issubclass(a, models.Model): if isinstance(a, type) and issubclass(a, models.Model):
model = a model = a
@@ -691,10 +854,12 @@ class Session:
return fq.values(*args) return fq.values(*args)
return None return None
def flush(self): def flush(self) -> None:
"""flush() 的别名,等同于 commit()。"""
self.commit() self.commit()
def execute(self, stmt): def execute(self, stmt) -> None:
"""执行语句(支持 _DeleteStmt 和原始 SQL 字符串)。"""
if isinstance(stmt, _DeleteStmt): if isinstance(stmt, _DeleteStmt):
stmt.execute() stmt.execute()
elif isinstance(stmt, str): elif isinstance(stmt, str):
@@ -703,14 +868,31 @@ class Session:
cursor.execute(stmt) cursor.execute(stmt)
# ---------------------------------------------------------------------------
# 全局入口
# ---------------------------------------------------------------------------
class DB: class DB:
"""全局数据库操作入口。
用法::
from gvsdsdk.fluent import db
session = db.session
q = db.or_(Model.field == 1, Model.field == 2)
q = db.and_(Model.field > 0, Model.field < 100)
"""
@property @property
def session(self): def session(self) -> Session:
"""获取当前线程的 Session 实例。"""
return _get_session() return _get_session()
@staticmethod @staticmethod
def or_(*args): def or_(*args: Q) -> Q:
result = None """逻辑 OR合并多个 Q 对象。"""
result: Optional[Q] = None
for q in args: for q in args:
if isinstance(q, Q): if isinstance(q, Q):
if result is None: if result is None:
@@ -720,7 +902,8 @@ class DB:
return result if result is not None else Q() return result if result is not None else Q()
@staticmethod @staticmethod
def and_(*args): def and_(*args: Q) -> Q:
"""逻辑 AND合并多个 Q 对象。"""
result = Q() result = Q()
for q in args: for q in args:
if isinstance(q, Q): if isinstance(q, Q):
@@ -731,5 +914,6 @@ class DB:
db = DB() db = DB()
def FQ(model_class): def FQ(model_class: Type[_M]) -> FluentQuery[_M]:
"""FluentQuery 工厂函数,等同于 ``Model.query``。"""
return FluentQuery(model_class) return FluentQuery(model_class)

View File

@@ -1,58 +1,81 @@
"""QModel 兼容层基类模块
提供 SQLAlchemy 风格的链式 OOP 查询接口,使 Django ORM 支持:
- ``Model.query.filter(...)`` — 链式 FluentQuery 查询
- ``Model.field_name`` — 类级别返回 FieldExpression支持 ``==``, ``>``, ``in_()`` 等运算符)
- ``Model.field_name`` — 实例级别返回字段值(与原生 Django Model 一致)
- ``Model[condition]`` — 快捷 filter 语法
典型用法::
# 链式查询
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
# 字段表达式
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1, 2, 3]))
# 聚合
Dingdan.query.aggregate(total=Sum('jine'))
# 分页
Dingdan.query.paginate(page=1, per_page=20)
"""
from __future__ import annotations from __future__ import annotations
import typing import re
from typing import Any, Dict, List, Optional, Tuple, Type, Union from typing import Any, Callable, Dict, Generic, Optional, Self, Tuple, Type, TypeVar, Union
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from .fluent import FluentQuery from .fluent import FluentQuery
class classproperty: # ---------------------------------------------------------------------------
def __init__(self, func): # 工具函数
# ---------------------------------------------------------------------------
#: classproperty 返回值类型变量
_RT = TypeVar('_RT')
#: QModel 子类类型变量(仅用于 __class_getitem__ 兼容)
_QM = TypeVar('_QM', bound='QModel')
class classproperty(Generic[_RT]):
"""类属性描述符,支持 ``Model.query`` 等类级别属性访问。
泛型参数 ``_RT`` 传播函数返回类型,使 Pylance 能穿透描述符推断
``Model.query`` 的返回类型为 ``FluentQuery[Self]``。
"""
def __init__(self, func: Callable[[type], _RT]) -> None:
self.fget = func self.fget = func
def __get__(self, obj, objtype=None): def __get__(self, obj: Any, objtype: Optional[type] = None) -> _RT:
return self.fget(objtype if objtype is not None else type(obj)) return self.fget(objtype if objtype is not None else type(obj))
ABBREVIATIONS = { ABBREVIATIONS: Dict[str, str] = {
'uuid': 'UUID', 'uuid': 'UUID', 'id': 'ID', 'ip': 'IP', 'url': 'URL', 'uri': 'URI',
'id': 'ID', 'api': 'API', 'html': 'HTML', 'css': 'CSS', 'js': 'JS', 'json': 'JSON',
'ip': 'IP', 'xml': 'XML', 'sql': 'SQL', 'http': 'HTTP', 'https': 'HTTPS',
'url': 'URL', 'smtp': 'SMTP', 'ssl': 'SSL', 'tls': 'TLS', 'ssh': 'SSH', 'ftp': 'FTP',
'uri': 'URI', '2fa': '2FA', 'elt': 'ELT', 'sve': 'SVE', 't4': 'T4', 'mc': 'MC',
'api': 'API', 'crb': 'CRB', 'ccgo': 'CCGO', 'ugm': 'UGM', 'gerp': 'GERP',
'html': 'HTML', 'abac': 'ABAC', 'rbac': 'RBAC', 'smtm': 'SMTM', 'pwd': 'PWD',
'css': 'CSS',
'js': 'JS',
'json': 'JSON',
'xml': 'XML',
'sql': 'SQL',
'http': 'HTTP',
'https': 'HTTPS',
'smtp': 'SMTP',
'ssl': 'SSL',
'tls': 'TLS',
'ssh': 'SSH',
'ftp': 'FTP',
'2fa': '2FA',
'elt': 'ELT',
'sve': 'SVE',
't4': 'T4',
'mc': 'MC',
'crb': 'CRB',
'ccgo': 'CCGO',
'ugm': 'UGM',
'gerp': 'GERP',
'abac': 'ABAC',
'rbac': 'RBAC',
'smtm': 'SMTM',
'pwd': 'PWD',
} }
def snake_to_pascal(name): def snake_to_pascal(name: str) -> str:
"""将 snake_case 转换为 PascalCase自动识别常见缩写。
>>> snake_to_pascal('user_uuid')
'UserUUID'
>>> snake_to_pascal('paifa_dingdan_shu')
'PaifaDingdanShu'
"""
if '_' not in name: if '_' not in name:
return name[0].upper() + name[1:] if name else name return name[0].upper() + name[1:] if name else name
parts = name.split('_') parts = name.split('_')
@@ -66,46 +89,74 @@ def snake_to_pascal(name):
return ''.join(result) return ''.join(result)
def pascal_to_snake(name): def pascal_to_snake(name: str) -> str:
import re """将 PascalCase 转换为 snake_case。
>>> pascal_to_snake('UserUUID')
'user_uuid'
"""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
parts = s2.lower().split('_') return s2.lower()
return '_'.join(parts)
# ---------------------------------------------------------------------------
# 字段表达式(类级别访问 Model.field 时返回)
# ---------------------------------------------------------------------------
class _InvertedFieldExpression: class _InvertedFieldExpression:
def __init__(self, field_expr): """取反字段表达式,由 ``~Model.field`` 产生,支持 ``~field.startswith(...)`` 等否定查询。"""
def __init__(self, field_expr: FieldExpression) -> None:
self._field_expr = field_expr self._field_expr = field_expr
def startswith(self, value): def startswith(self, value: str) -> Q:
return ~Q(**{f'{self._field_expr._django_name}__startswith': value}) return ~Q(**{f'{self._field_expr._django_name}__startswith': value})
def contains(self, value): def contains(self, value: str) -> Q:
return ~Q(**{f'{self._field_expr._django_name}__contains': value}) return ~Q(**{f'{self._field_expr._django_name}__contains': value})
def like(self, pattern): def like(self, pattern: str) -> Q:
clean = pattern.strip('%') clean = pattern.strip('%')
return ~Q(**{f'{self._field_expr._django_name}__icontains': clean}) return ~Q(**{f'{self._field_expr._django_name}__icontains': clean})
def in_(self, values): def in_(self, values) -> Q:
return ~Q(**{f'{self._field_expr._django_name}__in': values}) return ~Q(**{f'{self._field_expr._django_name}__in': values})
def is_(self, other): def is_(self, other) -> Q:
if other is None: if other is None:
return ~Q(**{f'{self._field_expr._django_name}__isnull': True}) return ~Q(**{f'{self._field_expr._django_name}__isnull': True})
return ~Q(**{self._field_expr._django_name: other}) return ~Q(**{self._field_expr._django_name: other})
def between(self, low, high) -> Q:
return ~(Q(**{f'{self._field_expr._django_name}__gte': low})
& Q(**{f'{self._field_expr._django_name}__lte': high}))
def __repr__(self) -> str:
return f'~{self._field_expr!r}'
class FieldExpression: class FieldExpression:
_resolve_cache = {} """字段表达式,由类级别 ``Model.field_name`` 访问产生。
def __init__(self, model_class, field_name): 支持通过 Python 运算符构建 Django Q 对象::
Model.field == value → Q(field=value)
Model.field > value → Q(field__gt=value)
Model.field.in_([1,2,3]) → Q(field__in=[1,2,3])
Model.field.between(a, b) → Q(field__gte=a) & Q(field__lte=b)
~Model.field → _InvertedFieldExpression否定查询
"""
_resolve_cache: Dict[Tuple[str, str], str] = {}
def __init__(self, model_class: Type, field_name: str) -> None:
self.model_class = model_class self.model_class = model_class
self.field_name = field_name self.field_name = field_name
self._django_name = self._resolve_django_name() self._django_name: str = self._resolve_django_name()
def _resolve_django_name(self): def _resolve_django_name(self) -> str:
"""将 Python 字段名解析为 Django ORM 查询使用的字段名。"""
cache_key = (self.model_class.__name__, self.field_name) cache_key = (self.model_class.__name__, self.field_name)
if cache_key in FieldExpression._resolve_cache: if cache_key in FieldExpression._resolve_cache:
return FieldExpression._resolve_cache[cache_key] return FieldExpression._resolve_cache[cache_key]
@@ -117,243 +168,232 @@ class FieldExpression:
FieldExpression._resolve_cache[cache_key] = self.field_name FieldExpression._resolve_cache[cache_key] = self.field_name
return self.field_name return self.field_name
def __eq__(self, other): # ---- 比较运算符 ----
def __eq__(self, other) -> Q: # type: ignore[override]
if isinstance(other, FieldExpression): if isinstance(other, FieldExpression):
return NotImplemented return NotImplemented
if other is None: if other is None:
return Q(**{f'{self._django_name}__isnull': True}) return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other}) return Q(**{self._django_name: other})
def __ne__(self, other): def __ne__(self, other) -> Q: # type: ignore[override]
if isinstance(other, FieldExpression): if isinstance(other, FieldExpression):
return NotImplemented return NotImplemented
return ~Q(**{self._django_name: other}) return ~Q(**{self._django_name: other})
def __gt__(self, other): def __gt__(self, other) -> Q:
return Q(**{f'{self._django_name}__gt': other}) return Q(**{f'{self._django_name}__gt': other})
def __ge__(self, other): def __ge__(self, other) -> Q:
return Q(**{f'{self._django_name}__gte': other}) return Q(**{f'{self._django_name}__gte': other})
def __lt__(self, other): def __lt__(self, other) -> Q:
return Q(**{f'{self._django_name}__lt': other}) return Q(**{f'{self._django_name}__lt': other})
def __le__(self, other): def __le__(self, other) -> Q:
return Q(**{f'{self._django_name}__lte': other}) return Q(**{f'{self._django_name}__lte': other})
def __getitem__(self, key): def __hash__(self) -> int:
return _JsonKeyExpression(self._django_name, key)
def __hash__(self):
return hash((self.model_class.__name__, self.field_name)) return hash((self.model_class.__name__, self.field_name))
def __and__(self, other): # ---- 逻辑运算符 ----
def __and__(self, other) -> Q:
if isinstance(other, Q): if isinstance(other, Q):
return Q(self) & other return Q(self) & other
if isinstance(other, FieldExpression):
return Q(self) & Q(other)
return NotImplemented return NotImplemented
def __or__(self, other): def __or__(self, other) -> Q:
if isinstance(other, Q): if isinstance(other, Q):
return Q(self) | other return Q(self) | other
if isinstance(other, FieldExpression):
return Q(self) | Q(other)
return NotImplemented return NotImplemented
def __invert__(self): def __invert__(self) -> _InvertedFieldExpression:
return _InvertedFieldExpression(self) return _InvertedFieldExpression(self)
def is_(self, other): # ---- 查询方法 ----
def is_(self, other) -> Q:
"""``field IS NULL`` 或 ``field IS value``。"""
if other is None: if other is None:
return Q(**{f'{self._django_name}__isnull': True}) return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other}) return Q(**{self._django_name: other})
def is_not(self, other): def is_not(self, other) -> Q:
"""``field IS NOT NULL`` 或 ``field IS NOT value``。"""
if other is None: if other is None:
return Q(**{f'{self._django_name}__isnull': False}) return Q(**{f'{self._django_name}__isnull': False})
return ~Q(**{self._django_name: other}) return ~Q(**{self._django_name: other})
def like(self, pattern): def like(self, pattern: str) -> Q:
"""模糊匹配(不区分大小写),自动去除 ``%`` 通配符。"""
clean = pattern.strip('%') clean = pattern.strip('%')
return Q(**{f'{self._django_name}__icontains': clean}) return Q(**{f'{self._django_name}__icontains': clean})
def contains(self, value): def contains(self, value: str) -> Q:
"""包含子串(区分大小写)。"""
return Q(**{f'{self._django_name}__contains': value}) return Q(**{f'{self._django_name}__contains': value})
def ilike(self, pattern): def ilike(self, pattern: str) -> Q:
"""like() 的别名。"""
return self.like(pattern) return self.like(pattern)
def startswith(self, value): def startswith(self, value: str) -> Q:
"""前缀匹配(区分大小写)。"""
return Q(**{f'{self._django_name}__startswith': value}) return Q(**{f'{self._django_name}__startswith': value})
def istartswith(self, value): def istartswith(self, value: str) -> Q:
"""前缀匹配(不区分大小写)。"""
return Q(**{f'{self._django_name}__istartswith': value}) return Q(**{f'{self._django_name}__istartswith': value})
def endswith(self, value): def endswith(self, value: str) -> Q:
"""后缀匹配(区分大小写)。"""
return Q(**{f'{self._django_name}__endswith': value}) return Q(**{f'{self._django_name}__endswith': value})
def iendswith(self, value): def iendswith(self, value: str) -> Q:
"""后缀匹配(不区分大小写)。"""
return Q(**{f'{self._django_name}__iendswith': value}) return Q(**{f'{self._django_name}__iendswith': value})
def in_(self, values): def in_(self, values) -> Q:
"""``field IN (values)``,支持列表和 FluentQuery 子查询。"""
if hasattr(values, '_qs'): if hasattr(values, '_qs'):
return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)}) return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)})
return Q(**{f'{self._django_name}__in': values}) return Q(**{f'{self._django_name}__in': values})
def between(self, low, high): def between(self, low, high) -> Q:
"""``field BETWEEN low AND high``。"""
return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high}) return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high})
def regex(self, pattern): def regex(self, pattern: str) -> Q:
"""正则匹配(区分大小写)。"""
return Q(**{f'{self._django_name}__regex': pattern}) return Q(**{f'{self._django_name}__regex': pattern})
def iregex(self, pattern): def iregex(self, pattern: str) -> Q:
"""正则匹配(不区分大小写)。"""
return Q(**{f'{self._django_name}__iregex': pattern}) return Q(**{f'{self._django_name}__iregex': pattern})
def desc(self): # ---- 排序辅助 ----
def desc(self) -> str:
"""返回降序排序字符串 ``'-field_name'``。"""
return f'-{self._django_name}' return f'-{self._django_name}'
def asc(self): def asc(self) -> str:
"""返回升序排序字符串 ``'field_name'``。"""
return self._django_name return self._django_name
def __repr__(self): # ---- JSON 字段访问 ----
def __getitem__(self, key: str) -> _JsonKeyExpression:
"""访问 JSON 字段内的键,如 ``Model.meta['key'] == value``。"""
return _JsonKeyExpression(self._django_name, key)
# ---- 显示 ----
def __repr__(self) -> str:
return f'FieldExpr({self.model_class.__name__}.{self.field_name})' return f'FieldExpr({self.model_class.__name__}.{self.field_name})'
def __str__(self): def __str__(self) -> str:
return self._django_name return self._django_name
class _JsonKeyExpression: class _JsonKeyExpression:
def __init__(self, field_name, key): """JSON 字段键表达式,由 ``Model.json_field['key']`` 产生。"""
def __init__(self, field_name: str, key: str) -> None:
self.field_name = field_name self.field_name = field_name
self.key = key self.key = key
def _lookup(self, suffix=''): def _lookup(self, suffix: str = '') -> str:
if suffix: if suffix:
return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}' return f'{self.field_name}__{self.key}'
def cast(self, type_hint): def cast(self, type_hint) -> _CastJsonExpression:
"""将 JSON 值转换为指定类型后再比较。"""
return _CastJsonExpression(self.field_name, self.key, type_hint) return _CastJsonExpression(self.field_name, self.key, type_hint)
def __eq__(self, other): def __eq__(self, other) -> Q: # type: ignore[override]
return Q(**{self._lookup(): other}) return Q(**{self._lookup(): other})
def __ne__(self, other): def __ne__(self, other) -> Q: # type: ignore[override]
return ~Q(**{self._lookup(): other}) return ~Q(**{self._lookup(): other})
def __gt__(self, other): def __gt__(self, other) -> Q:
return Q(**{self._lookup('gt'): other}) return Q(**{self._lookup('gt'): other})
def __ge__(self, other): def __ge__(self, other) -> Q:
return Q(**{self._lookup('gte'): other}) return Q(**{self._lookup('gte'): other})
def __lt__(self, other): def __lt__(self, other) -> Q:
return Q(**{self._lookup('lt'): other}) return Q(**{self._lookup('lt'): other})
def __le__(self, other): def __le__(self, other) -> Q:
return Q(**{self._lookup('lte'): other}) return Q(**{self._lookup('lte'): other})
def between(self, low, high): def between(self, low, high) -> Q:
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __repr__(self): def __repr__(self) -> str:
return f'JsonKeyExpr({self.field_name}[{self.key!r}])' return f'JsonKeyExpr({self.field_name}[{self.key!r}])'
class _CastJsonExpression: class _CastJsonExpression:
def __init__(self, field_name, key, type_hint): """类型转换后的 JSON 键表达式。"""
def __init__(self, field_name: str, key: str, type_hint) -> None:
self.field_name = field_name self.field_name = field_name
self.key = key self.key = key
self.type_hint = type_hint self.type_hint = type_hint
def _lookup(self, suffix=''): def _lookup(self, suffix: str = '') -> str:
if suffix: if suffix:
return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}' return f'{self.field_name}__{self.key}'
def between(self, low, high): def __eq__(self, other) -> Q: # type: ignore[override]
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __eq__(self, other):
return Q(**{self._lookup(): other}) return Q(**{self._lookup(): other})
def __ne__(self, other): def __ne__(self, other) -> Q: # type: ignore[override]
return ~Q(**{self._lookup(): other}) return ~Q(**{self._lookup(): other})
def __gt__(self, other): def __gt__(self, other) -> Q:
return Q(**{self._lookup('gt'): other}) return Q(**{self._lookup('gt'): other})
def __ge__(self, other): def __ge__(self, other) -> Q:
return Q(**{self._lookup('gte'): other}) return Q(**{self._lookup('gte'): other})
def __lt__(self, other): def __lt__(self, other) -> Q:
return Q(**{self._lookup('lt'): other}) return Q(**{self._lookup('lt'): other})
def __le__(self, other): def __le__(self, other) -> Q:
return Q(**{self._lookup('lte'): other}) return Q(**{self._lookup('lte'): other})
def __repr__(self): def between(self, low, high) -> Q:
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __repr__(self) -> str:
type_name = getattr(self.type_hint, '__name__', str(self.type_hint)) type_name = getattr(self.type_hint, '__name__', str(self.type_hint))
return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})' return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})'
class ForeignKeyIdDescriptor: # ---------------------------------------------------------------------------
"""FK 字段描述符:类级别访问返回 FieldExpression实例级别访问/设置委托给原始 Django 字段。""" # QModel 元类与基类
def __init__(self, fk_name): # ---------------------------------------------------------------------------
self.fk_name = fk_name
self.id_attr = fk_name + '_id'
self._original_field = None # 延迟绑定原始 Django 字段
def _get_original_field(self, owner):
if self._original_field is None:
try:
self._original_field = owner._meta.get_field(self.fk_name)
except Exception:
pass
return self._original_field
def __get__(self, obj, objtype=None):
if obj is None:
return FieldExpression(objtype, self.id_attr)
return getattr(obj, self.id_attr)
def __set__(self, obj, value):
# 直接写入实例 __dict__避免触发描述符递归
obj.__dict__[self.id_attr] = value
class FieldExpressionDescriptor:
"""普通字段描述符:类级别访问返回 FieldExpression实例级别访问/设置委托给原始 Django 字段。"""
def __init__(self, field_name):
self.field_name = field_name
self._original_field = None # 延迟绑定原始 Django 字段
def _get_original_field(self, owner):
if self._original_field is None:
try:
self._original_field = owner._meta.get_field(self.field_name)
except Exception:
pass
return self._original_field
def __get__(self, obj, objtype=None):
if obj is None:
return FieldExpression(objtype, self.field_name)
# 实例级别:从 __dict__ 获取,或让 Django 字段描述符处理
if self.field_name in obj.__dict__:
return obj.__dict__[self.field_name]
# 委托给原始 Django 字段
orig = self._get_original_field(objtype)
if orig is not None:
return orig.__get__(obj, objtype)
return getattr(obj, self.field_name)
def __set__(self, obj, value):
# 直接写入实例 __dict__避免触发描述符递归
obj.__dict__[self.field_name] = value
class QModelBase(models.base.ModelBase): class QModelBase(models.base.ModelBase):
def __new__(mcs, name, bases, namespace, **kwargs): """QModel 元类,负责:
# 不再强制设置 db_column —— Django 会自动处理
1. 根据 ``GVSDSDK_MANAGE_TABLES`` 设置动态控制 ``managed``
2. 包装字段描述符,使类级别访问返回 FieldExpression
"""
def __new__(mcs, name, bases, namespace, **kwargs):
# 动态设置 managed子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表 # 动态设置 managed子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表
meta = namespace.get('Meta') meta = namespace.get('Meta')
if meta and getattr(meta, 'app_label', None) == 'gvsdsdk': if meta and getattr(meta, 'app_label', None) == 'gvsdsdk':
@@ -378,12 +418,12 @@ def _wrap_field_descriptors(cls: Type) -> None:
实例级别完全委托给 Django 原生描述符。 实例级别完全委托给 Django 原生描述符。
Django 的字段描述符DeferredAttribute / ForwardManyToOneDescriptor Django 的字段描述符DeferredAttribute / ForwardManyToOneDescriptor
- __get__(None, cls) → 返回字段对象本身(类级别) - ``__get__(None, cls)`` → 返回字段对象本身(类级别)
- __get__(instance, cls) → 返回字段值(实例级别) - ``__get__(instance, cls)`` → 返回字段值(实例级别)
我们包装后: 我们包装后:
- __get__(None, cls) → 返回 FieldExpression类级别用于链式 OOP - ``__get__(None, cls)`` → 返回 FieldExpression类级别用于链式 OOP
- __get__(instance, cls) → 委托给原生描述符(实例级别,正常行为) - ``__get__(instance, cls)`` → 委托给原生描述符(实例级别,正常行为)
""" """
from django.db.models.query_utils import DeferredAttribute from django.db.models.query_utils import DeferredAttribute
from django.db.models.fields.related_descriptors import ( from django.db.models.fields.related_descriptors import (
@@ -408,13 +448,18 @@ def _wrap_field_descriptors(cls: Type) -> None:
class _FieldExpressionWrapper: class _FieldExpressionWrapper:
"""包装 Django 字段描述符,类级别返回 FieldExpression实例级别委托给原生描述符。""" """包装 Django 字段描述符,类级别返回 FieldExpression实例级别委托给原生描述符。
def __init__(self, original_descriptor, model_class, field_name): 这是 QModel 字段表达式机制的核心:当你在类上访问 ``Model.field_name`` 时,
返回 ``FieldExpression`` 对象,支持 ``Model.field == value`` 等运算符构建 Q 对象;
当你在实例上访问 ``obj.field_name`` 时,行为与原生 Django 完全一致。
"""
def __init__(self, original_descriptor, model_class: Type, field_name: str) -> None:
self._original = original_descriptor self._original = original_descriptor
self._model = model_class self._model = model_class
self._field_name = field_name self._field_name = field_name
self._has_set = hasattr(original_descriptor, '__set__') self._has_set: bool = hasattr(original_descriptor, '__set__')
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
if obj is None: if obj is None:
@@ -423,7 +468,7 @@ class _FieldExpressionWrapper:
# 实例级别访问:委托给原生 Django 描述符 # 实例级别访问:委托给原生 Django 描述符
return self._original.__get__(obj, objtype) return self._original.__get__(obj, objtype)
def __set__(self, obj, value): def __set__(self, obj, value) -> None:
if self._has_set: if self._has_set:
# FK/O2O 描述符有 __set__ # FK/O2O 描述符有 __set__
self._original.__set__(obj, value) self._original.__set__(obj, value)
@@ -431,14 +476,19 @@ class _FieldExpressionWrapper:
# 普通字段DeferredAttribute没有 __set__直接写 __dict__ # 普通字段DeferredAttribute没有 __set__直接写 __dict__
obj.__dict__[self._field_name] = value obj.__dict__[self._field_name] = value
def __repr__(self) -> str:
return f'_FieldExpressionWrapper({self._model.__name__}.{self._field_name})'
class QModel(models.Model, metaclass=QModelBase): class QModel(models.Model, metaclass=QModelBase):
"""QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。 """QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。
所有继承 QModel 的模型自动获得: 所有继承 QModel 的模型自动获得:
- ``Model.query`` : 返回 FluentQuery支持链式查询 - ``Model.query`` : 返回 FluentQuery支持链式查询
- ``Model.field_name`` : 类级别访问返回 FieldExpression支持表达式查询 - ``Model.field_name`` : 类级别访问返回 FieldExpression支持表达式查询
- ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致) - ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致)
- ``Model[condition]`` : 快捷 filter 语法
用法示例:: 用法示例::
@@ -456,28 +506,30 @@ class QModel(models.Model, metaclass=QModelBase):
# 分页 # 分页
Dingdan.query.paginate(page=1, per_page=20) Dingdan.query.paginate(page=1, per_page=20)
# 快捷 filter
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
""" """
class Meta: class Meta:
abstract = True abstract = True
@classproperty @classproperty
def query(cls) -> FluentQuery: def query(cls) -> FluentQuery[Self]:
"""返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。""" """返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。"""
return FluentQuery(cls) return FluentQuery(cls)
@classmethod @classmethod
def __class_getitem__(cls, item) -> FluentQuery: def __class_getitem__(cls, item) -> FluentQuery[Self]:
"""支持 Model[condition] 语法,返回 filter 后的 FluentQuery。 """支持 ``Model[condition]`` 语法,返回 filter 后的 FluentQuery。
用法:: 用法::
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list() Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
Dingdan[{'zhuangtai': 8}].to_list()
""" """
if isinstance(item, Q): if isinstance(item, Q):
return cls.query.filter(item) return cls.query.filter(item)
if isinstance(item, dict): if isinstance(item, dict):
return cls.query.filter(**item) return cls.query.filter(**item)
return cls.query.filter(item) return cls.query.filter(item)

View File

@@ -174,7 +174,7 @@ class UserManager(BaseUserManager):
class User(QModel): class User(QModel):
"""用户表——系统核心身份模型(映射 SAAS 主库 User 表)""" """用户表——系统核心身份模型(映射 SAAS 主库 User 表)"""
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=uuid.uuid4) UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=_uuid.uuid4)
UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名') UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名')
UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码') UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码')
UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态') UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态')

View File

@@ -12,10 +12,10 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from backend.models import ( from backend.models import (
DashouRiTongji, PlayerDailyStats,
GuanshiRiTongji, ManagerDailyStats,
ZuzhangRiTongji, LeaderDailyStats,
ShangjiaRiTongji, MerchantDailyStats,
) )
from users.models import UserDashou, UserShangjia, UserBoss from users.models import UserDashou, UserShangjia, UserBoss
from gvsdsdk.models import User from gvsdsdk.models import User
@@ -29,28 +29,32 @@ SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'})
ROLE_CONFIG = { ROLE_CONFIG = {
'dashou': { 'dashou': {
'model': DashouRiTongji, 'model': PlayerDailyStats,
'sort_field': 'chengjiao_zonge', 'id_field': 'PlayerID',
'fields': ('jiedan_zongliang', 'chengjiao_zongliang', 'jiedan_zonge', 'chengjiao_zonge'), 'sort_field': 'CompletedAmount',
'int_fields': frozenset({'jiedan_zongliang', 'chengjiao_zongliang'}), 'fields': ('AcceptedOrderTotal', 'CompletedOrderTotal', 'AcceptedAmount', 'CompletedAmount'),
'int_fields': frozenset({'AcceptedOrderTotal', 'CompletedOrderTotal'}),
}, },
'guanshi': { 'guanshi': {
'model': GuanshiRiTongji, 'model': ManagerDailyStats,
'sort_field': 'shouru_zonge', 'id_field': 'ManagerID',
'fields': ('yaoqing_dashou_shu', 'chongzhi_dashou_shu', 'shouru_zonge'), 'sort_field': 'TotalIncome',
'int_fields': frozenset({'yaoqing_dashou_shu', 'chongzhi_dashou_shu'}), 'fields': ('InvitedPlayerCount', 'RechargedPlayerCount', 'TotalIncome'),
'int_fields': frozenset({'InvitedPlayerCount', 'RechargedPlayerCount'}),
}, },
'zuzhang': { 'zuzhang': {
'model': ZuzhangRiTongji, 'model': LeaderDailyStats,
'sort_field': 'shouru_zonge', 'id_field': 'LeaderID',
'fields': ('yaoqing_guanshi_shu', 'fenyong_jine', 'shouru_zonge'), 'sort_field': 'TotalIncome',
'int_fields': frozenset({'yaoqing_guanshi_shu'}), 'fields': ('InvitedManagerCount', 'CommissionAmount', 'TotalIncome'),
'int_fields': frozenset({'InvitedManagerCount'}),
}, },
'shangjia': { 'shangjia': {
'model': ShangjiaRiTongji, 'model': MerchantDailyStats,
'sort_field': 'jiesuan_jine', 'id_field': 'MerchantID',
'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), 'sort_field': 'SettledAmount',
'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), 'fields': ('AssignedOrderCount', 'AssignedAmount', 'SettledOrderCount', 'SettledAmount'),
'int_fields': frozenset({'AssignedOrderCount', 'SettledOrderCount'}),
}, },
} }
@@ -137,10 +141,11 @@ class PhbHqsjView(APIView):
nick_map, avatar_map = self._load_user_display(rows, shenfen) nick_map, avatar_map = self._load_user_display(rows, shenfen)
cfg = ROLE_CONFIG[shenfen] cfg = ROLE_CONFIG[shenfen]
int_fields = cfg['int_fields'] int_fields = cfg['int_fields']
id_field = cfg['id_field']
result_list = [] result_list = []
for idx, row in enumerate(rows): for idx, row in enumerate(rows):
yonghuid = row['yonghuid'] yonghuid = row[id_field]
item = { item = {
'mingci': idx + 1, 'mingci': idx + 1,
'yonghuid': yonghuid, 'yonghuid': yonghuid,
@@ -169,35 +174,37 @@ class PhbHqsjView(APIView):
model = cfg['model'] model = cfg['model']
sort_field = cfg['sort_field'] sort_field = cfg['sort_field']
fields = cfg['fields'] fields = cfg['fields']
id_field = cfg['id_field']
annotate_kwargs = {f: Sum(f) for f in fields} annotate_kwargs = {f: Sum(f) for f in fields}
if all_time: if all_time:
rows = ( rows = (
model.objects model.objects
.values('yonghuid') .values(id_field)
.annotate(**annotate_kwargs) .annotate(**annotate_kwargs)
.order_by(f'-{sort_field}')[:MAX_RANK] .order_by(f'-{sort_field}')[:MAX_RANK]
) )
return list(rows) return list(rows)
if start_date == end_date: if start_date == end_date:
qs = model.objects.filter(riqi=start_date).order_by(f'-{sort_field}')[:MAX_RANK] qs = model.objects.filter(Date=start_date).order_by(f'-{sort_field}')[:MAX_RANK]
return [ return [
{'yonghuid': r.yonghuid, **{f: getattr(r, f) for f in fields}} {id_field: getattr(r, id_field), **{f: getattr(r, f) for f in fields}}
for r in qs for r in qs
] ]
rows = ( rows = (
model.objects model.objects
.filter(riqi__gte=start_date, riqi__lte=end_date) .filter(Date__gte=start_date, Date__lte=end_date)
.values('yonghuid') .values(id_field)
.annotate(**annotate_kwargs) .annotate(**annotate_kwargs)
.order_by(f'-{sort_field}')[:MAX_RANK] .order_by(f'-{sort_field}')[:MAX_RANK]
) )
return list(rows) return list(rows)
def _load_user_display(self, rows, shenfen): def _load_user_display(self, rows, shenfen):
yonghuids = [r['yonghuid'] for r in rows if r.get('yonghuid')] id_field = ROLE_CONFIG[shenfen]['id_field']
yonghuids = [r[id_field] for r in rows if r.get(id_field)]
if not yonghuids: if not yonghuids:
return {}, {} return {}, {}

View File

@@ -31,7 +31,7 @@ from config.models import Szjilu, TixianQuotaDefault
from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.fadan_utils import check_fadan_qiangdan_eligible
from products.models import Huiyuangoumai from products.models import Huiyuangoumai
from backend.models import TixianRiTongji from backend.models import WithdrawalDailyStats
from utils.wechat_v3 import build_authorization from utils.wechat_v3 import build_authorization
@@ -445,12 +445,12 @@ def _get_active_auto_record(shenhe_danhao):
def _get_platform_daily_total(leixing, today=None): def _get_platform_daily_total(leixing, today=None):
"""平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" """平台今日已提现总额:只读 houtai.WithdrawalDailyStats(到账金额口径)"""
if today is None: if today is None:
today = date.today() today = date.today()
try: try:
return TixianRiTongji.query.get(riqi=today, leixing=leixing).total_amount return WithdrawalDailyStats.query.get(Date=today, WithdrawalType=leixing).total_amount
except TixianRiTongji.DoesNotExist: except WithdrawalDailyStats.DoesNotExist:
return decimal.Decimal('0.00') return decimal.Decimal('0.00')
@@ -576,7 +576,7 @@ def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_d
def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''):
""" """
收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。
锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。 锁定 WithdrawalDailyStats + 用户行,校验通过后立即写入统计,防止高并发超额。
本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。
返回 (True, '', '') 或 (False, msg, 'personal'|'platform') 返回 (True, '', '') 或 (False, msg, 'personal'|'platform')
""" """
@@ -617,9 +617,9 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
'personal', 'personal',
) )
platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create(
riqi=today, Date=today,
leixing=leixing, WithdrawalType=leixing,
defaults={'total_amount': decimal.Decimal('0.00')}, defaults={'total_amount': decimal.Decimal('0.00')},
) )
platform_limit = _get_platform_daily_limit(leixing) platform_limit = _get_platform_daily_limit(leixing)
@@ -678,15 +678,15 @@ def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
zuzhang.save(update_fields=['jinri_tixian']) zuzhang.save(update_fields=['jinri_tixian'])
try: try:
stat = TixianRiTongji.objects.select_for_update().get( stat = WithdrawalDailyStats.objects.select_for_update().get(
riqi=today, leixing=leixing, Date=today, WithdrawalType=leixing,
) )
stat.total_amount = max( stat.total_amount = max(
stat.total_amount - shijidaozhang, stat.total_amount - shijidaozhang,
decimal.Decimal('0.00'), decimal.Decimal('0.00'),
) )
stat.save(update_fields=['total_amount']) stat.save(update_fields=['total_amount'])
except TixianRiTongji.DoesNotExist: except WithdrawalDailyStats.DoesNotExist:
pass pass
except Exception as e: except Exception as e:
logger.error( logger.error(
@@ -741,7 +741,7 @@ def _quota_already_reserved_for_record(auto_record):
def apply_transfer_success_quota(auto_record): def apply_transfer_success_quota(auto_record):
""" """
打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。
- houtai.TixianRiTongji.total_amount += 实际到账 - houtai.WithdrawalDailyStats.total_amount += 实际到账
- 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长)
须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。
""" """
@@ -758,9 +758,9 @@ def apply_transfer_success_quota(auto_record):
logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid)
return return
platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create(
riqi=today, Date=today,
leixing=leixing, WithdrawalType=leixing,
defaults={'total_amount': decimal.Decimal('0.00')}, defaults={'total_amount': decimal.Decimal('0.00')},
) )
platform_stat.total_amount += shijidaozhang platform_stat.total_amount += shijidaozhang

View File

@@ -84,7 +84,7 @@ from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
) )
from config.models import Qunpeizhi, Szjilu from config.models import Qunpeizhi, Szjilu
from backend.models import ShangjiaRiTongji from backend.models import MerchantDailyStats
from rank.models import ( from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu, KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao KaoheCishuFeiyong, YonghuChenghao
@@ -1088,13 +1088,13 @@ class ShangJiaXinXiView(APIView):
}) })
response_data['chenghao_list'] = chenghao_list response_data['chenghao_list'] = chenghao_list
# ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- # ------------------- 3. 从 MerchantDailyStats 获取今日统计 -------------------
try: try:
today_stat = ShangjiaRiTongji.query.get(yonghuid=yonghuid, riqi=today) today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today)
jinri_paifa_dingdan = today_stat.paifa_dingdan_shu jinri_paifa_dingdan = today_stat.AssignedOrderCount
jinri_paifa_jine = float(today_stat.paifa_jine) jinri_paifa_jine = float(today_stat.AssignedAmount)
jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu jinri_tuikuan_dingdan = today_stat.RefundOrderCount
except ShangjiaRiTongji.DoesNotExist: except MerchantDailyStats.DoesNotExist:
jinri_paifa_dingdan = 0 jinri_paifa_dingdan = 0
jinri_paifa_jine = 0.00 jinri_paifa_jine = 0.00
jinri_tuikuan_dingdan = 0 jinri_tuikuan_dingdan = 0
@@ -1103,14 +1103,14 @@ class ShangJiaXinXiView(APIView):
response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
# ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- # ------------------- 4. 从 MerchantDailyStats 获取本月统计 -------------------
month_stats = ShangjiaRiTongji.query.filter( month_stats = MerchantDailyStats.query.filter(
yonghuid=yonghuid, MerchantID=yonghuid,
riqi__gte=this_month_start, Date__gte=this_month_start,
riqi__lte=today Date__lte=today
).aggregate( ).aggregate(
total_paifa_jine=Sum('paifa_jine'), total_paifa_jine=Sum('AssignedAmount'),
total_paifa_dingdan=Sum('paifa_dingdan_shu') total_paifa_dingdan=Sum('AssignedOrderCount')
) )
yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
# yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加 # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加